Two Factor Authentication V2
This PR is based on #5397 and will therefore close it. Introduces basic two-factor authentication (2FA) using Time-based One-Time Passwords (TOTP) support for Icinga Web 2.
TOTP (Time-based One-Time Password) is a type of two-factor authentication (2FA) method. It generates short-lived numeric codes (usually 6-8 digits) based on:
- A shared secret key (known to both the server and your authenticator app).
- The current time (in 30-second intervals by default).
Since the code changes frequently and is only valid for a short window, it makes accounts harder to hack, even if someone steals your password.
After enabling 2FA, users authenticate with their usual Icinga Web credentials and are then prompted to provide a valid TOTP code from their authenticator app (e.g., Google Authenticator, Authy).
We will use the defaults of 6 digit tokens, 30 seconds interval and sha-1 algorithm for token generation. The generated token is valid for 10 seconds before and after the current time to allow some clock drift.
Current Authentication Process
Currently, Icinga Web authentication follows this workflow:
- First it checks if there is set a remember me cookie from past logins.
- If so, it attempts to log the user in using the encrypted credentials stored in the cookie.
- If there is no remember me cookie set, it will show a login form.
- If submitted it will compare the password with the hash in the database.
- If correct it will setup the user and store it in the database.
- Additionally it will create the remember me cookie if the slider for that in the login form is checked.
- Then it will redirect to the dashboard or another target set by url param
redirect.
2FA Implementation
This implementation is split in two main parts.
- The configuration of 2FA via the account settings
- The actual authentication process with token input
Configuration
The configuration of TOTP-based 2FA is fully integrated into the user’s account settings. A new form (TwoFactorConfigForm) allows users to enable 2FA, verify, or remove a TOTP secret directly from their account settings.
When a user with the required permission (user/two-factor-authentication) accesses their account, the AccountController loads the current TOTP secret from the database (TwoFactorTotp::loadFromDb()). If none exists, a new secret is generated (TwoFactorTotp::generate()) and passed into the form.
The form (TwoFactorConfigForm) then guides the user through the following workflow:
-
Enable 2FA: If no TOTP secret is stored, the user can opt in via a checkbox. Once enabled, a new secret is created, and the form displays both a QR code (rendered by
TwoFactorTotp::createQRCode()) and a manual provisioning URI. The user can scan this code with an authenticator app or copy the URI via a copy-to-clipboard element. -
Verification Step: To prevent misconfiguration, the user must enter a valid TOTP code generated by their app. The code is checked using the verify method of the
TwoFactorTotpclass. Only if the verification succeeds will the secret be persisted to the database (saveToDb()). -
Remove 2FA: If a secret already exists in the database, the form instead provides the option to remove it. On form submit, the stored secret is deleted (
removeFromDb()). (Should be extended in the future to ask for confirmation!)
Authentication
Once a user has a TOTP secret stored, the login flow gains a second step. After the username and password are verified, the session sets a 2fa_must_challenge_token flag, stores a temporary user object, and if “Stay logged in” was checked, a temporary remember-me cookie in the session. The remember-me cookie is not persisted yet. This prevents bypassing 2FA by simply reusing the cookie. Auth::isAuthenticated() also enforces that a user is only considered fully logged in once the TOTP challenge has succeeded, unless explicitly skipped for trusted cookies.
The LoginForm is rewritten now extending ipl\Web\Compat\CompatForm and decides which elements it should render. If the 2fa_must_challenge_token flag is set in the session, the elements to verify the TOTP token are displayed, otherwise the normal login form elements. A valid remember-me cookie can bypass both, the password login and the token challenge: if present and verified, the cookie is renewed, persisted, and the user is logged in.
When the LoginForm is submitted via the btn_submit_verify_2fa submit button, the secret is loaded from the database and the entered code is verified. A successful check sets $twoFactorSuccessful to true on the user object that was temporarily stored in the session after password login, clears the 2fa_must_challenge_token session flag, persists any deferred remember-me cookie, and calls AuthenticationHook::triggerLogin(). Submitting the form via the btn_submit_cancel_2fa button lets users abort, which purges the session and resets the login.
Future
Possible extensions for the future could be:
- Encrypt secrets in the database
- Option for administrators to enforce 2fa for users
- Ask for password verification to add or remove a TOTP secret in the account settings
Requires GD php extension for QR code generation Requires https://github.com/Icinga/icinga-php-thirdparty/pull/53 Closes #5397
The failed phpcs tests are not due to my changes.
Requires https://github.com/Icinga/icinga-php-thirdparty/pull/53
I'm afraid I have to convert this PR to draft in this case.
The failed phpcs tests are not due to my changes.
Well, the current master is all-✅.
... or personal access token? (idea (c) @TheSyscall)