edunext-eu / simplesamlphp-module-simpletotp
A highly configurable yet simple to use TOTP based two-factor authentication processing module for SimpleSAMLphp
Package info
github.com/edunext-eu/SimpleTOTP
Type:simplesamlphp-module
pkg:composer/edunext-eu/simplesamlphp-module-simpletotp
Requires
- php: >=7.4
- ext-ctype: *
- ext-hash: *
- ext-json: *
- simplesamlphp/composer-module-installer: ~1.0
This package is auto-updated.
Last update: 2026-07-29 15:04:18 UTC
README
A SimpleSAMLphp auth processing filter that adds TOTP-based MFA on an IdP or SP. Recommended placement is the IdP to keep TOTP secrets off SPs. Only HMAC-SHA1 TOTP is supported for maximum compatibility with authenticator apps. Current release: 2.1. See UPGRADING.md when updating from 2.0.
Key features
- Works as an authproc filter (IdP or SP).
- Configurable secret attribute and validation timeout.
- Optional clock-drift window for TOTP verification.
- Atomic, credential-bound brute-force throttling.
- TOTP replay protection and one-time authentication state.
- Automatic removal of the TOTP secret from outbound attributes.
- Optional bypass when secret is an empty string.
Installation
Via Git
Clone into the SimpleSAMLphp modules/ directory.
Via Composer
Install the module from Packagist:
composer require edunext-eu/simplesamlphp-module-simpletotp
Quick start (IdP recommended)
Add the filter to authproc.idp after the authentication source supplies the
seed and before filters that rename identifiers. For example, when
core:AttributeMap runs at priority 35:
'authproc.idp' => [ 34 => [ 'class' => 'simpletotp:mfa', 'secret_attr' => 'totp_secret', 'enforce_mfa' => false, 'allow_empty_secret' => false, 'totp_window' => 0, 'max_attempts' => 5, 'attempt_window' => 300, 'totp_rate_limit_storage' => 'session', 'totp_rate_limit_key' => 'uid', 'restart_enabled' => false, 'clear_cookies' => [], ], 35 => [ 'class' => 'core:AttributeMap', 'name2oid', ], ],
validation_timeout is intentionally omitted above: its default is 60
minutes. It caches the fact that MFA succeeded for the same SimpleSAMLphp
session, authentication context, identity, and TOTP seed. It does not extend
the validity of a six-digit TOTP code.
For a Redis-backed deployment, use the following values in the filter:
'totp_rate_limit_storage' => 'both', 'totp_rate_limit_key' => 'uid', 'restart_enabled' => true, 'clear_cookies' => [], // Optional when the surrounding state has no suitable restart URL: //'restart_url' => 'https://idp.example.org/your-login-entry-point',
TOTP secret handling
secret_attr is the long-lived Base32 seed (for example, ga_secret), not
the six-digit code entered by the user. Version 2.1 removes that attribute as
soon as the simpletotp:mfa filter runs, including cached, optional-MFA, and
error paths. When a prompt is required, the seed exists only in saved
server-side authentication state and is removed before the authproc chain
resumes.
With the recommended IdP-side placement:
- the seed is not included in the SAML assertion and is never sent to an SP;
- no downstream
core:AttributeAltercleanup filter is required; and - the TOTP filter should not also be installed at each SP.
Place this filter after the authentication source has supplied the seed but
before filters such as core:AttributeMap that rename the user identifier
used for rate limiting. Filters that run later cannot read secret_attr.
Module enablement
Enable the module via config.php by setting:
'module.enable' => [ 'simpletotp' => true, ],
This is preferred over the legacy modules/<name>/enable file because it is explicit
and visible in configuration management.
SP-side verification (legacy; not recommended)
SP-side verification remains possible. In that deployment model:
- Do not run
simpletotp:mfaat the IdP. If the IdP filter runs, it removes the seed before producing the SAML assertion. - Configure the IdP to release the seed attribute only to the specific, trusted SP that needs it.
- Run the filter in that SP's
authproc.spchain:
'authproc.sp' => [ 10 => [ 'class' => 'simpletotp:mfa', 'secret_attr' => 'totp_secret', 'enforce_mfa' => false, 'allow_empty_secret' => false, 'totp_rate_limit_storage' => 'session', ], ],
The SP-side filter consumes and removes the seed before the SP's downstream processing or application receives the attributes. This mode therefore still works, but the seed crosses the federation boundary and every receiving SP becomes responsible for protecting it.
Never send the user's six-digit TOTP code to an SP. If the SP only needs to know that MFA occurred, communicate an authentication-assurance result (for example, an agreed SAML AuthnContext or assertion attribute) rather than the seed or code. SimpleTOTP does not currently create that assurance signal.
Configuration notes
- secret_attr: defaults to
totp_secret. If your attributes use a different name (e.g.ga_secret), set it explicitly. The module always removes this attribute before authentication processing continues. - enforce_mfa: when true, users without a configured secret are blocked (or redirected to
not_configured_url). - allow_empty_secret: when true, an empty-string secret is treated as "not configured" and the user is allowed to continue.
- Interaction:
allow_empty_secretonly applies when the secret attribute exists but is an empty string. It does not overrideenforce_mfafor truly missing secrets. - Null vs empty: "missing" means the attribute is absent or null, while "empty" means the attribute exists but the value is an empty string. This lets you use empty strings as an explicit "MFA disabled" flag.
- Recommended for DB-backed secrets: if your DB uses empty strings (e.g.,
two_factor_secret = '') for "no MFA", setallow_empty_secret = trueand keepenforce_mfa = false. For strict MFA, setenforce_mfa = trueand require non-empty secrets. - validation_timeout: minutes to cache a successful MFA before re-prompting (default 60). The cache is bound to the authentication context, subject, and current TOTP secret, so switching accounts, IdPs/auth sources, or rotating the secret cannot reuse it. This setting does not change the 30-second lifetime of a TOTP code.
- totp_window: number of 30-second steps to accept before/after the current step.
- 0 = only the current 30s step (strict, most secure)
- 1 = accept codes from 30s before or after (90s total window)
- 2 = accept codes from 60s before or after (150s total window)
- Values above 10 are rejected as unsafe configuration.
- max_attempts / attempt_window: throttles brute-force attempts across browser sessions using atomic state below the configured SimpleSAMLphp
cachedir. - Form fields: new integrations should post
totpwithautocomplete="one-time-code";codeis accepted for legacy forms. - Start over button: set
restart_enabled = truein thesimpletotp:mfaauthproc configuration. Setrestart_urlto the trusted URL that initiates a fresh login; when omitted, SimpleSAMLphp's state restart URL is used if available.- Restart accepts POST only, requires a one-time state-bound token, consumes the old authentication state, and only redirects to a server-side URL accepted by SimpleSAMLphp.
- If
clear_cookiesis omitted or empty, the module clears the active SimpleSAMLphp session-handler cookie andsession.authtoken.cookienameusing their configured path, domain, Secure, HTTP-only, and SameSite attributes.
- TOTP rate limit storage: the historical
sessionvalue now uses an atomic local counter keyed by the credential, so clearing cookies does not reset it. Settotp_rate_limit_storagetostoreorbothto add shared state across application servers.- Redis uses atomic Lua operations and is recommended for clustered deployments.
- SQL and memcache use the SimpleSAMLphp Store API plus a per-node lock. This prevents worker races on each server, but the Store API has no compare-and-set operation; Redis is required for an exact cluster-wide maximum.
- If the configured key is missing at runtime, the limiter falls back to the TOTP secret and logs a warning.
totp_rate_limit_keyuses built-in types (uid,secret,ip) orattr:<name>to refer to a specific attribute (e.g.attr:mail,attr:eduPersonPrincipalName). The attribute must exist in the user attributes for your IdP/SP, otherwise it falls back to the secret.- For federations that standardize identifiers (e.g.
eduPersonPrincipalName,eduPersonUniqueID,subject-id,pairwise-id), prefer one of those stable identifiers viaattr:<name>. - If
store.typeis unavailable or set tophpsession, verification remains protected by the atomic local limiter and a warning is logged. - If
cachediris unavailable on an older installation, the module falls back to SimpleSAMLphp's legacy temporary-directory helper. - TOTP codes are fixed at 6 digits.
- A successfully accepted time slice cannot be reused for the same credential.
clear_cookiesonly uses admin-configured cookie names; it is not accepted from user input.- Cookie clearing only affects the current host; it does not clear cookies set on other subdomains or parent domains.
Security notes
- TOTP seeds should remain on the IdP. Version 2.1 removes the configured seed attribute immediately when this filter runs.
- Keep totp_window small (default 0) to reduce acceptance of old codes.
- Atomic brute-force throttling and replay protection are built in.
- Verification uses a timing-safe comparison, strict canonical Base32 decoding, fixed-length checking, bounded secret input, and 64-bit counter packing. Primitive hardening was reviewed and backported from https://github.com/poetter-sebastian/SimpleThenticator/tree/main where compatible.
Translations
All user-facing strings are translatable via gettext. Add or edit translations in
locales/<lang>/LC_MESSAGES/simpletotp.po (e.g. locales/it/LC_MESSAGES/simpletotp.po or
locales/es/LC_MESSAGES/simpletotp.po). SimpleSAMLphp selects the language based on the
user's locale settings.
Translations are best-effort and may need review by native speakers; some locales may still use English strings. Pull requests or issues to refine wording are welcome.
Fork notice
This repository is maintained at https://github.com/edunext-eu/SimpleTOTP and is a fork
of the original module. If you are upgrading from the original, update your Composer
package name to edunext-eu/simplesamlphp-module-simpletotp.
Third-party TOTP primitive notices are retained in
THIRD_PARTY_NOTICES.md.
Changes in this fork
- Version 2.1 strips the secret automatically, isolates cached MFA by credential, rejects TOTP replay, consumes successful state, and hardens restart and rate limiting.
- Fixed MFA bypass by setting lastverified only on successful TOTP verification.
- Updated BadRequest class for newer SimpleSAMLphp.
- Added empty-code validation and removed sensitive debug logging.
- Added totp_window for clock drift tolerance.
- Added rate limiting for TOTP attempts.
- Removed the legacy token generator endpoint.
- Tightened StateId handling to accept only GET/POST (no generic $_REQUEST).
- Added timing-safe code comparison and stricter base32 validation.
- Added translations and documentation improvements.
- Removed the legacy
default-enablefile; usemodule.enablein config.php instead.
Maintenance
Long-term maintenance for this module is not guaranteed. If you want to take stewardship, open an issue.
Testing
Run composer test, or execute the four PHP scripts under tests/
directly. The suite covers RFC vectors, strict Base32 parsing, parallel rate
limiting, replay rejection, credential-bound MFA caching, state consumption,
and restart CSRF/cookie behavior.
Disclaimer
This software is provided "as is" without warranty of any kind; use at your own risk.