PHPackages                             sandstorm/neostwofactorauthentication - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. sandstorm/neostwofactorauthentication

ActiveNeos-package[Authentication &amp; Authorization](/categories/authentication)

sandstorm/neostwofactorauthentication
=====================================

3.0.0(1w ago)1327.0k↓55.7%14[1 issues](https://github.com/sandstorm/NeosTwoFactorAuthentication/issues)MITPHPPHP ^8.2CI failing

Since Feb 8Pushed 6d ago8 watchersCompare

[ Source](https://github.com/sandstorm/NeosTwoFactorAuthentication)[ Packagist](https://packagist.org/packages/sandstorm/neostwofactorauthentication)[ RSS](/packages/sandstorm-neostwofactorauthentication/feed)WikiDiscussions main Synced yesterday

READMEChangelog (10)Dependencies (25)Versions (36)Used By (0)

Neos Backend 2FA
================

[](#neos-backend-2fa)

Extend the Neos backend login to support second factors. We support TOTP tokens (Authenticator apps) and WebAuthn / FIDO2 hardware security keys (e.g. Yubikey).

What this package does
----------------------

[](#what-this-package-does)

    Screen.Recording.2022-02-08.at.17.07.59.mov    This package allows all users to register their personal second factor — either a TOTP token (Authenticator App) or a hardware security key (Yubikey / WebAuthn). Users can register one of each and pick which to use at login. As an Administrator you are able to delete factors for users again, in case they locked themselves out.

### Security keys (WebAuthn / FIDO2)

[](#security-keys-webauthn--fido2)

Browsers expose WebAuthn only over `https://` or on `localhost`. Make sure the Neos backend is served over HTTPS in production, otherwise the security-key flow will fail.

Configure the relying party identifier when your backend hostname differs from the registered domain:

```
Sandstorm:
  NeosTwoFactorAuthentication:
    webAuthn:
      relyingPartyName: 'My CMS'
      # null means: derive from the request hostname (works for same-origin deployments).
      # Set to the registrable domain ('example.com') if you serve the backend from a subdomain
      # and want credentials to be usable across subdomains.
      relyingPartyId: null
      # Default is 'discouraged' so FIDO U2F-only authenticators (e.g. YubiKey 4) work
      # via the browser's U2F-compat fallback. Set to 'preferred' or 'required' to
      # demand PIN/biometric — note that 'required' excludes U2F-only keys.
      userVerification: 'discouraged'
      timeout: 60000
```

#### Attestation

[](#attestation)

There is no setting for attestation. We always request the `none` conveyance preference, so the browser does not return identifying attestation data about the authenticator. Only the `none` and `fido-u2f` attestation statement formats are accepted when loading a credential (the latter is required for U2F-only authenticators registered via the browser's U2F-compat fallback). Other attestation statement types are not supported yet.

#### Authenticator compatibility

[](#authenticator-compatibility)

Authenticator`userVerification: discouraged``userVerification: required`YubiKey 5 / FIDO2 keys✅ touch✅ PIN + touchYubiKey 4 / older U2F-only keys✅ touch (U2F-compat)❌ not supportedPlatform authenticators (Touch ID, Windows Hello)✅ biometric✅ biometric[![Screenshot 2022-02-08 at 17 11 01](https://user-images.githubusercontent.com/12086990/153028043-93e9220e-cc22-4879-9edb-3e156c9accc8.png)](https://user-images.githubusercontent.com/12086990/153028043-93e9220e-cc22-4879-9edb-3e156c9accc8.png)

Versioning Scheme
-----------------

[](#versioning-scheme)

Package VersionNeos / Flow VersionReleased?SupportedRemarks3.x9.x, 8.x.✅✅`main` branch2.x9.x, 8.x, 7.x✅`main` branch1.x9.x, 8.x, 7.x, 3.x✅Settings
--------

[](#settings)

### Enforce 2FA

[](#enforce-2fa)

To enforce the setup and usage of 2FA you can add the following to your `Settings.yaml`.

```
Sandstorm:
  NeosTwoFactorAuthentication:
    # enforce 2FA for all users
    enforceTwoFactorAuthentication: true
```

With this setting, no user can login into the CMS without setting up a second factor first.

In addition, you can enforce 2FA for specific authentication providers and/or roles by adding following to your `Settings.yaml`

```
Sandstorm:
  NeosTwoFactorAuthentication:
    # enforce 2FA for specific authentication providers
    enforce2FAForAuthenticationProviders: ["Neos.Neos:Backend"]
    # enforce 2FA for specific roles
    enforce2FAForRoles: ["Neos.Neos:Administrator"]
```

### Issuer Naming

[](#issuer-naming)

To override the default sitename as issuer label, you can define one via the configuration settings:

```
Sandstorm:
  NeosTwoFactorAuthentication:
    # (optional) if set this will be used as a naming convention for the TOTP. If empty the Site name will be used
    issuerName: ""
```

### TOTP leeway

[](#totp-leeway)

By default, TOTP codes are verified against the current 30-second window only, with no tolerance for clock drift between the user's device and the server. If users occasionally hit "invalid code" errors near the boundary of a code's lifetime, you can allow some drift via:

```
Sandstorm:
  NeosTwoFactorAuthentication:
    # Acceptable TOTP clock drift in seconds. Codes from (now - leeway) through (now + leeway)
    # are accepted. 0 disables leeway (exact match only). MUST be lower than the 30s TOTP period;
    # values >= 30 are clamped to 29.
    totpLeewayInSeconds: 5
```

Tested 2FA apps
---------------

[](#tested-2fa-apps)

Thx to @Sebobo @Benjamin-K for creating a list of supported and testet apps!

**iOS**:

- Google Authenticator (used for development) ✅
- Authy ✅
- Microsoft Authenticator ✅
- 1Password ✅

**Android**:

- Google Authenticator ✅
- Microsoft Authenticator ✅
- Authy ✅

How we did it
-------------

[](#how-we-did-it)

- We introduced a new middleware `SecondFactorMiddleware` which handles 2FA on a Neos `Session` basis.
    - This is an overview of the checks the `SecondFactorMiddleware` does for any request:

        ```
                                ┌─────────────────────────────┐
                                │           Request           │
                                └─────────────────────────────┘
                                               ▼
                                    ... middleware chain ...
                                               ▼
                                ┌───────────────────────────────┐
                                │  SecurityEntryPointMiddleware │
                                └───────────────────────────────┘
                                               ▼
                ┌───────────────────────────────────────────────────────────────────┐
                │                     SecondFactorMiddleware                        │
                │                                                                   │
                │  ┌─────────────────────────────────────────────────────────────┐  │
                │  │ 1. Skip, if no authentication tokens are present, because   │  │
                │  │    we're not on a secured route.                            │  │
                │  └─────────────────────────────────────────────────────────────┘  │
                │  ┌─────────────────────────────────────────────────────────────┐  │
                │  │ 2. Skip, if 'Neos.Backend:Backend' authentication token not │  │
                │  │    present, because we only support second factors for Neos │  │
                │  │    backend.                                                 │  │
                │  └─────────────────────────────────────────────────────────────┘  │
                │  ┌─────────────────────────────────────────────────────────────┐  │
                │  │ 3. Skip, if 'Neos.Backend:Backend' authentication token is  │  │
                │  │    not authenticated, because we need to be authenticated   │  │
                │  │    with the authentication provider of                      │  │
                │  │    'Neos.Backend:Backend' first.                            │  │
                │  └─────────────────────────────────────────────────────────────┘  │
                │  ┌─────────────────────────────────────────────────────────────┐  │
                │  │ 4. Skip, if second factor is not set up for account and not │  │
                │  │    enforced via settings.                                   │  │
                │  └─────────────────────────────────────────────────────────────┘  │
                │  ┌─────────────────────────────────────────────────────────────┐  │
                │  │ 5. Skip, if second factor is already authenticated.         │  │
                │  └─────────────────────────────────────────────────────────────┘  │
                │  ┌─────────────────────────────────────────────────────────────┐  │
                │  │ 6. Redirect to 2FA login, if second factor is set up for    │  │
                │  │    account but not authenticated.                           │  │
                │  │    Skip, if already on 2FA login route.                     │  │
                │  └─────────────────────────────────────────────────────────────┘  │
                │  ┌─────────────────────────────────────────────────────────────┐  │
                │  │ 7. Redirect to 2FA setup, if second factor is not set up for│  │
                │  │    account but is enforced by system.                       │  │
                │  │    Skip, if already on 2FA setup route.                     │  │
                │  └─────────────────────────────────────────────────────────────┘  │
                │  ┌─────────────────────────────────────────────────────────────┐  │
                │  │ X. Throw an error, because any check before should have     │  │
                │  │    succeeded.                                               │  │
                │  └─────────────────────────────────────────────────────────────┘  │
                └───────────────────────────────────────────────────────────────────┘
                                                  ▼
                                         ... middlewares ...

        ```

When updating Neos, those part will likely crash:
-------------------------------------------------

[](#when-updating-neos-those-part-will-likely-crash)

- the login screen for the second factor is a hard copy of the login screen from the `Neos.Neos` package
    - just replaced the username/password form with the form for the second factor
    - maybe has to be replaced when neos gets updated
- hopefully the rest of this package is solid enough to survive the next mayor Neos versions ;)

Why not ...?
------------

[](#why-not-)

### Enhance the `UsernamePassword` authentication token

[](#enhance-the-usernamepassword-authentication-token)

> This actually has been the approach up until version 1.0.5.

One issue with this is the fact, that we *want* the user to be logged in with that token via the `PersistedUsernamePasswordProvider`, but at the same time to *not be logged in* with that token as long as 2FA is not authenticated as well. We found it hard to find a secure way to model the 2FA setup solution when 2FA is enforced, but the user does not have a second factor enabled, yet.

The middleware approach makes a clear distinction between "Logging in" and "Second Factor Authentication", while still being session based and unable to bypass.

### Set the authenticationStrategy to `allTokens`

[](#set-the-authenticationstrategy-to-alltokens)

The AuthenticationProviderManager requires to authorize all tokens at the same time otherwise, it will throw an Exception (see AuthenticationProviderManager Line 181

```
if ($this->authenticationStrategy === Context::AUTHENTICATE_ALL_TOKENS) {
    throw new AuthenticationRequiredException('Could not authenticate all tokens, but authenticationStrategy was set to "all".', 1222203912);
}
```

)

This leads to an error where the `AuthenticationProviderManager` throws exceptions before the user is able to enter any credentials. The `SecurityEntryPointMiddleware` catches those exceptions and redirects to the Neos Backend Login, which causes the same exception again. We get caught in an endless redirect.

The [Neos Flow Security Documentation](https://flowframework.readthedocs.io/en/stable/TheDefinitiveGuide/PartIII/Security.html#multi-factor-authentication-strategy)suggests how to implement a multi-factor-authentication, but this method seems like it was never tested. At the moment of writing it seems like the `authenticationStrategy: allTokens` flag is broken and not usable.

Contributing
------------

[](#contributing)

### Testing

[](#testing)

The package ships with end-to-end tests built on [Playwright](https://playwright.dev) and written in Gherkin syntax via [playwright-bdd](https://vitalets.github.io/playwright-bdd/).

#### Running the tests

[](#running-the-tests)

Tests require Docker and Node.js. All Makefile targets are run from `Tests/E2E/`. Run the initial setup once — it builds the SUT images and installs the test dependencies (if [nvm](https://github.com/nvm-sh/nvm) is available it will automatically switch to the Node version from `.nvmrc`):

```
make setup          # build SUT images + install test dependencies
make setup-test     # only install node dependencies, playwright and generate BDD files
```

Re-generate Playwright spec files whenever a `.feature` file changes:

```
make generate-bdd-files
```

Run the tests:

```
make test                   # run all tests (neos8 + neos9, all configurations)

make test-neos8             # run all neos8 tests
make test-neos8-defaults    # default configuration only
make test-neos8-enforce-all # enforceTwoFactorAuthentication: true
make test-neos8-enforce-role
make test-neos8-enforce-provider

make test-neos9             # same targets for neos9 / PHP 8.5

make sut-prune              # tear down all docker compose environments and remove volumes
```

Run `make help` to see all available targets.

#### Debugging tests

[](#debugging-tests)

To debug a test, run the test from `Tests/E2E/` with flags like this:

- `npm run test:neos8:enforce-all -- --debug` - to run the test in headed mode with Playwright Inspector
- `npm run test:neos8:enforce-all -- --ui` - to run the test in headed mode with Playwright Test Runner UI

If you just want to see the test running in the browser just `npm run test:neos8:enforce-all -- --headed`.

> While debugging you can also enter the SUT with `make enter-sut-neos8` and `make enter-sut-neos9` respectively.
>
> You can even the tests you want to debug with `npm run test:neos8:enforce-all -- --grep @debug` and adding the `@debug` tag to the scenario you want to debug. But using the --ui flag is usually more convenient for debugging.

#### System under test (SUT)

[](#system-under-test-sut)

There are two docker compose environments in `Tests/system_under_test/`:

- `neos8/` — Neos with PHP 8.2
- `neos9/` — Neos with PHP 8.5

Both are built from the repository root as the Docker build context, so the local package source is copied into the container and installed via a Composer path repository. This means every test run tests the *current working tree* of the package, not a published version.

#### Configuration variants

[](#configuration-variants)

The `FLOW_CONTEXT` environment variable is passed into the docker compose environment via variable substitution, and Flow's hierarchical configuration loading picks up the corresponding `Settings.yaml` from the SUT:

Playwright tag`FLOW_CONTEXT`What is tested`@default-context``Production/E2E-SUT`No enforcement — 2FA is optional`@enforce-for-all``Production/E2E-SUT/EnforceForAll``enforceTwoFactorAuthentication: true``@enforce-for-role``Production/E2E-SUT/EnforceForRole`Enforcement scoped to `Neos.Neos:Administrator``@enforce-for-provider``Production/E2E-SUT/EnforceForProvider`Enforcement scoped to an authentication provider#### Test isolation

[](#test-isolation)

Each scenario starts with a clean state. An `AfterScenario` hook runs after every scenario to:

1. Log the browser out via a POST to `/neos/logout`
2. Delete all Neos users (`./flow user:delete --assume-yes '*'`)

Deleting all users also cascades to their 2FA devices, so no separate cleanup step is needed. Users and devices are re-created by the Background steps at the start of each scenario.

#### Design decisions

[](#design-decisions)

**Gherkin / BDD over plain Playwright specs** — the feature files document the intended behaviour of each configuration variant at a level that is readable without knowing the implementation. The generated Playwright spec files (`.features-gen/`) are not committed; they are re-generated by `bddgen` before each test run.

**UI-only device enrolment** — 2FA devices are enrolled through the browser UI (the backend module or the setup page) rather than a dedicated CLI command. This avoids coupling the tests to internal persistence details and exercises the same enrolment path a real user would take. The `deviceNameSecretMap` in `helpers/state.ts` carries TOTP secrets across steps within a scenario (e.g. from the enrolment step to the OTP entry step).

**Sequential execution** — tests run with `workers: 1` and `fullyParallel: false` because all scenarios share a single running SUT container and a single database. Running them in parallel would cause interference between scenarios.

**User creation via `docker exec`** — Neos user creation is done through the Flow CLI (`./flow user:create`) rather than the UI because the UI path is not part of what this package tests, and using the CLI is faster and more reliable for setup.

###  Health Score

61

—

FairBetter than 98% of packages

Maintenance97

Actively maintained with recent releases

Popularity38

Limited adoption so far

Community24

Small or concentrated contributor base

Maturity74

Established project with proven stability

 Bus Factor1

Top contributor holds 57.8% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~88 days

Recently: every ~158 days

Total

19

Last Release

11d ago

Major Versions

1.5.0 → 2.0.02026-03-18

2.0.1 → 3.0.02026-06-22

PHP version history (3 changes)1.0.0PHP ^7.4 | ^8.0

2.0.0PHP ^8.1

3.0.0PHP ^8.2

### Community

Maintainers

![](https://www.gravatar.com/avatar/2ced0d63cfdae881c32128c7f66451a013d3e24d9eed210d6a846b6d8e95fa3b?d=identicon)[sandstorm](/maintainers/sandstorm)

---

Top Contributors

[![JamesAlias](https://avatars.githubusercontent.com/u/1615332?v=4)](https://github.com/JamesAlias "JamesAlias (74 commits)")[![Pingu501](https://avatars.githubusercontent.com/u/12086990?v=4)](https://github.com/Pingu501 "Pingu501 (19 commits)")[![tobiasgruber](https://avatars.githubusercontent.com/u/15209886?v=4)](https://github.com/tobiasgruber "tobiasgruber (11 commits)")[![lorenzulrich](https://avatars.githubusercontent.com/u/1816023?v=4)](https://github.com/lorenzulrich "lorenzulrich (6 commits)")[![hphoeksma](https://avatars.githubusercontent.com/u/250683?v=4)](https://github.com/hphoeksma "hphoeksma (6 commits)")[![Nickosaurus](https://avatars.githubusercontent.com/u/113518385?v=4)](https://github.com/Nickosaurus "Nickosaurus (2 commits)")[![skurfuerst](https://avatars.githubusercontent.com/u/190777?v=4)](https://github.com/skurfuerst "skurfuerst (2 commits)")[![mberhorst](https://avatars.githubusercontent.com/u/2861236?v=4)](https://github.com/mberhorst "mberhorst (2 commits)")[![on3iro](https://avatars.githubusercontent.com/u/8681413?v=4)](https://github.com/on3iro "on3iro (1 commits)")[![Benjamin-K](https://avatars.githubusercontent.com/u/3098031?v=4)](https://github.com/Benjamin-K "Benjamin-K (1 commits)")[![anothanj](https://avatars.githubusercontent.com/u/26765837?v=4)](https://github.com/anothanj "anothanj (1 commits)")[![flammel](https://avatars.githubusercontent.com/u/3888?v=4)](https://github.com/flammel "flammel (1 commits)")[![bwaidelich](https://avatars.githubusercontent.com/u/307571?v=4)](https://github.com/bwaidelich "bwaidelich (1 commits)")[![adrian-cerdeira](https://avatars.githubusercontent.com/u/43271236?v=4)](https://github.com/adrian-cerdeira "adrian-cerdeira (1 commits)")

---

Tags

2faneosneos-cmstwo-factor-authentication

### Embed Badge

![Health badge](/badges/sandstorm-neostwofactorauthentication/health.svg)

```
[![Health](https://phpackages.com/badges/sandstorm-neostwofactorauthentication/health.svg)](https://phpackages.com/packages/sandstorm-neostwofactorauthentication)
```

###  Alternatives

[neos/neos

An open source Content Application Platform based on Flow. A set of core Content Management features is resting within a larger context that allows you to build a perfectly customized experience for your users.

1151.0M777](/packages/neos-neos)[appwrite/server-ce

End to end backend server for frontend and mobile apps.

56.4k108.1k](/packages/appwrite-server-ce)[craftcms/cms

Craft CMS

3.6k3.6M3.1k](/packages/craftcms-cms)[spatie/laravel-passkeys

Use passkeys in your Laravel app

471890.7k39](/packages/spatie-laravel-passkeys)[jeffgreco13/filament-breezy

A custom package for Filament with login flow, profile and teams support.

1.0k2.1M58](/packages/jeffgreco13-filament-breezy)[contao/core-bundle

Contao Open Source CMS

1231.6M2.8k](/packages/contao-core-bundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
