edunext-eu/simplesamlphp-module-simpletotp

A highly configurable yet simple to use TOTP based two-factor authentication processing module for SimpleSAMLphp

Maintainers

Package info

github.com/edunext-eu/SimpleTOTP

Type:simplesamlphp-module

pkg:composer/edunext-eu/simplesamlphp-module-simpletotp

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 1

2.1 2026-07-29 14:57 UTC

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:AttributeAlter cleanup 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:

  1. Do not run simpletotp:mfa at the IdP. If the IdP filter runs, it removes the seed before producing the SAML assertion.
  2. Configure the IdP to release the seed attribute only to the specific, trusted SP that needs it.
  3. Run the filter in that SP's authproc.sp chain:
'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_secret only applies when the secret attribute exists but is an empty string. It does not override enforce_mfa for 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", set allow_empty_secret = true and keep enforce_mfa = false. For strict MFA, set enforce_mfa = true and 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 totp with autocomplete="one-time-code"; code is accepted for legacy forms.
  • Start over button: set restart_enabled = true in the simpletotp:mfa authproc configuration. Set restart_url to 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_cookies is omitted or empty, the module clears the active SimpleSAMLphp session-handler cookie and session.authtoken.cookiename using their configured path, domain, Secure, HTTP-only, and SameSite attributes.
  • TOTP rate limit storage: the historical session value now uses an atomic local counter keyed by the credential, so clearing cookies does not reset it. Set totp_rate_limit_storage to store or both to 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_key uses built-in types (uid, secret, ip) or attr:<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 via attr:<name>.
    • If store.type is unavailable or set to phpsession, verification remains protected by the atomic local limiter and a warning is logged.
    • If cachedir is 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_cookies only 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-enable file; use module.enable in 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.