Skip to main content

Production-grade MFA orchestration library with policy-driven factor chaining

Project description

mfa-chain-orchestrator

Policy-driven MFA chain orchestration with strict step order, TOTP verification, and reset-on-failure behavior.

Features

  • fixed or random MFA factor sequencing per attempt.
  • Enforced required_steps with validated policy definitions.
  • TOTP verification via pyotp.
  • Strict ordering protection via MFAChainBreached.
  • Security hardening: any single failure returns RESET and forces restart from Token 1.

Installation

pip install -r requirements.txt

or

pip install .

Usage

from mfa_chain_orchestrator import MFAOrchestrator, Policy

policy = Policy(
    mode="random",
    required_steps=2,
    factors=[
        {"id": "token_1", "label": "Authenticator App", "type": "totp"},
        {"id": "token_2", "label": "Backup Device", "type": "totp"},
        {"id": "token_3", "label": "Hardware Token", "type": "totp"},
    ],
)

orchestrator = MFAOrchestrator(policy)
chain = orchestrator.initialize_attempt()
first = chain[0]

# Verify step 1
result = orchestrator.verify_step(
    secret="JBSWY3DPEHPK3PXP",  # Base32 secret
    user_input="123456",
    window=1,
    factor_id=first.id,
)

if not result.success and result.next_factor_label == "RESET":
    # Restart from Token 1 (call initialize_attempt again if desired)
    pass

FastAPI Session Integration Example

from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel

from mfa_chain_orchestrator import MFAChainBreached, MFAOrchestrator, Policy

app = FastAPI()

policy = Policy(
    mode="fixed",
    required_steps=2,
    factors=[
        {"id": "token_1", "label": "Token 1", "type": "totp"},
        {"id": "token_2", "label": "Token 2", "type": "totp"},
    ],
)

orchestrator_store: dict[str, MFAOrchestrator] = {}


class StepVerifyPayload(BaseModel):
    factor_id: str
    user_input: str


def get_orchestrator(session_id: str) -> MFAOrchestrator:
    if session_id not in orchestrator_store:
        orchestrator = MFAOrchestrator(policy)
        chain = orchestrator.initialize_attempt()
        # Persist chain metadata in your server-side session store if needed.
        orchestrator_store[session_id] = orchestrator
    return orchestrator_store[session_id]


@app.post("/mfa/verify")
def verify_step(payload: StepVerifyPayload, request: Request):
    session_id = request.headers.get("X-Session-Id")
    if not session_id:
        raise HTTPException(status_code=400, detail="Missing session id")

    orchestrator = get_orchestrator(session_id)

    try:
        result = orchestrator.verify_step(
            secret="JBSWY3DPEHPK3PXP",  # fetch per-user secret from secure storage
            user_input=payload.user_input,
            window=1,
            factor_id=payload.factor_id,
        )
    except MFAChainBreached as exc:
        raise HTTPException(status_code=409, detail=str(exc)) from exc

    if not result.success and result.next_factor_label == "RESET":
        # Any failure hard-resets chain state.
        orchestrator.initialize_attempt()
        raise HTTPException(status_code=401, detail="MFA reset. Start again from Token 1.")

    if result.is_complete:
        return {"authenticated": True}

    return {
        "authenticated": False,
        "next_factor_label": result.next_factor_label,
    }

Security Notes

  • verify_step() validates code shape (6 digits) before TOTP verification.
  • On any failure, cursor resets to the first step and returns next_factor_label="RESET".
  • Out-of-order calls can be blocked by passing factor_id; mismatches raise MFAChainBreached.
  • Store secrets in an HSM/KMS-backed vault, never in plaintext config.

Public API

  • MFAOrchestrator(policy: Policy)
  • MFAOrchestrator.initialize_attempt() -> list[FactorDefinition]
  • MFAOrchestrator.verify_step(secret: str, user_input: str, window: int, factor_id: str | None = None) -> Result
  • MFAChainBreached
  • Policy, FactorDefinition, Result

Runnable Test App (FastAPI)

A ready-to-run test app is included at examples/fastapi_test_app.py.

1. Install extra test-app dependencies

pip install fastapi uvicorn

Optional QR PNG payload support:

pip install qrcode[pil]

2. Run the app

PYTHONPATH=src uvicorn examples.fastapi_test_app:app --reload

3. Browser UI (recommended)

Open:

  • http://127.0.0.1:8000/ (or http://127.0.0.1:8000/ui)

The UI guides users through:

  • enrolling Google and Microsoft authenticator factors
  • starting a login attempt
  • verifying each MFA step in order

4. API-first test flow (curl)

4. Test the flow

Enroll two different authenticator apps for one user:

curl -s -X POST http://127.0.0.1:8000/users/alice/factors/google_auth/enroll
curl -s -X POST http://127.0.0.1:8000/users/alice/factors/ms_auth/enroll

Then start an attempt:

curl -s -X POST http://127.0.0.1:8000/users/alice/attempt/start

Get the currently expected factor + a debug code:

curl -s http://127.0.0.1:8000/attempt/<session_id>/debug/current-code

Verify:

curl -s -X POST http://127.0.0.1:8000/attempt/verify \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "<session_id>",
    "factor_id": "<expected factor_id>",
    "user_input": "<code>",
    "window": 1
  }'

If verification fails, the orchestrator returns reset behavior and the chain restarts from step 1. For the full scriptable process, see TEST_PROCESS.md.

License

This project is licensed under the GNU General Public License v3.0. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mfa_chain_orchestrator-0.1.0.tar.gz (17.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mfa_chain_orchestrator-0.1.0-py3-none-any.whl (18.6 kB view details)

Uploaded Python 3

File details

Details for the file mfa_chain_orchestrator-0.1.0.tar.gz.

File metadata

  • Download URL: mfa_chain_orchestrator-0.1.0.tar.gz
  • Upload date:
  • Size: 17.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for mfa_chain_orchestrator-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2a21256be84b5bdc6fd19d13aa6aa03c4de346bf4670bfd8bdaf33a728cf8c67
MD5 114c05faa4b81ffc6ae3190d3d49038b
BLAKE2b-256 517556ca7863e1a965dd23ac5ce6988427042db72767fd04f2045457a8fbb1c5

See more details on using hashes here.

Provenance

The following attestation bundles were made for mfa_chain_orchestrator-0.1.0.tar.gz:

Publisher: publish-pypi.yml on snowsky/mfa-chain-orchestrator

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file mfa_chain_orchestrator-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for mfa_chain_orchestrator-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4174c6fc8010e30d8fc45acea834a8e12e3edd40be9a680d283bbe0fd1b672b9
MD5 6a3ec5a28845f75192db2b996094fa5a
BLAKE2b-256 f404266b6fdc99d41102f00e3961db467c71d215ababdf3e14c6ca54d217b7b9

See more details on using hashes here.

Provenance

The following attestation bundles were made for mfa_chain_orchestrator-0.1.0-py3-none-any.whl:

Publisher: publish-pypi.yml on snowsky/mfa-chain-orchestrator

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page