Skip to content

JWT token auth, MFA/TOTP, and security module suite#6

Open
ztane wants to merge 141 commits into
masterfrom
jwt-token-auth
Open

JWT token auth, MFA/TOTP, and security module suite#6
ztane wants to merge 141 commits into
masterfrom
jwt-token-auth

Conversation

@ztane

@ztane ztane commented Jun 15, 2026

Copy link
Copy Markdown
Member

Summary

Adds a batteries-included authentication/authorization stack under tet.security, built on pyramid_di services and SQLAlchemy. Rebased onto the current src/tet/ layout master; the diff is clean (54 files, +6180/−123).

What's included

  • Tokens (tokens.py) — long-lived prefixed API tokens, generated with secrets.token_bytes(32) and stored SHA-256 hashed at rest.
  • JWT (authentication.py) — short-term JWTs with a pluggable signing secret via an ISecretCallback protocol (no hardcoded secret).
  • MFA / TOTP (mfa.py) — enrolment + verification with replay protection.
  • Rate limiting (rate_limit.py) and token cleanup.
  • Auth events + audit logging (events.py).
  • Policies (policy.py, authorization.py) — wrapping fixed to use providedBy over isinstance.
  • Config / wiring (config.py, __init__.py), models (models.py), Pyramid 2.0 compat (compat.py), views (views.py: login, JWT issue/refresh, MFA, password, token revoke).

Quality

  • ~99% test coverage (tests/services/security/…, public-API contract tests).
  • Sphinx autodoc for all security modules + a narrative security guide.
  • passlib / pyramid deprecation fixes; prep for 0.6a1.

Known pre-merge TODOs (tracked in-branch)

  • JWT auth fixes flagged before merge.
  • Return the refresh token in the response body too.

sevanteri and others added 30 commits June 15, 2026 21:24
- Add pyjwt to the list of dependencies
- Rename IUserAuthenticationService
- Use includeme instead of auth_include
- Add request to serect_callback
- Update docstrings.
- Use config.action() to register directives, enabling conflict detection.
- Improve variable names, headers, and other elements for clarity and consistency.
- Fix argument duplication in create_long_term_token call
- config: Setup the ACLAuthorizationPolicy before AuthenticationPolicy
- Move it to the root level directory
- Add pythonpath so it can identify the tests module itself
- Add test_requires to the dependencies
- Ensure the fixture does not doing extra works such as asserting on every request and return the desired values instead.
- Make it possible to set the name of long_term_token, and access_token headers.
…missions, improve logging and exception handling.
- Implementing the secrect, and login callback(s)
- Adding the route_prefix for callable module
- Adding more tests.
- Update models.
- Update requires packages in setup.py
- Update README
- Update the return type for the mock callback
- Meaningful name for the secret_callback
- Rename Ruff configuration
- Reverse the Ruff formatting
- Increase the line length for Ruff and format it.
- Add a dataclass for the default registered claims and include them in the declaratives.
- Update the payload claims for JWT encoding.
- Add postgresql for testing
- Update python matrix
- Ensure datetime with UTC work in python 3.9+
- Add more tests for the app that applied JWTCookieAuthenticationPolicy
- Add collection hooks to ignore test items if it does not match the required condition
- Add JWTCookieAuthenticationPolicy
- Automatically select the login view base on the security policy during the setup stage
- Ability to set cookie name of the refresh token and access token
- Add refresh token endpoint
- Set cookie for long term token only
- Ability to set max age of the long term token
- Bind the long term token cookie to specific route. e.g: /refresh route by default
ztane and others added 14 commits June 15, 2026 21:25
MultiFactorAuthMethodType() enum constructor raises ValueError for
invalid values, and all valid enum values are truthy, so the
`if not mfa_method_type` branch was unreachable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ance

zope Interface does not support Python's isinstance(); use
INewAuthorizationPolicy.providedBy() which correctly checks
whether an object implements the interface.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move json_body parsing after auth check in change_password
- Add error handling to mfa_verify (HTTPException passthrough + generic)
- Simplify mfa_method_type.value ternaries (enum is always truthy)
- Use RFC 7518-compliant HMAC key length in test secret

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use pyramid.authorization for Allow, Authenticated, Everyone, Deny
  (NO_PERMISSION_REQUIRED stays in pyramid.security — not moved yet)
- Use 32+ byte wrong key in invalid signature test to avoid
  InsecureKeyLengthWarning

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
passlib.hash.sha256_crypt.encrypt() was deprecated in Passlib 1.7
in favor of .hash().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…okens

Same pattern as change_password: parse request body after auth check
and inside try block so malformed requests still trigger audit events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- tests/models/accounts.py: use sqlalchemy.orm.declarative_base
- tet/sqlalchemy/password.py: use orm.declared_attr directly,
  remove redundant sqlalchemy.ext.declarative import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move MFA check before token creation to prevent orphaned tokens
- Use hmac.compare_digest for constant-time token comparison
- Use dataclasses.replace to avoid mutating shared CookieAttributes
- Read TOTP secret from DB instead of trusting client-provided setup_key
- Fix verify_password to call user.validate_password (matching UserPasswordMixin)
- Return HTTP 400 instead of None for unsupported MFA method types
- Update tests to match new signatures and behaviors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… 2.0 compat

- TOTP replay protection via UNLOGGED table + FOR UPDATE serialization
- Rate limiting on login with separate DB connection (survives tx rollback)
- Token cleanup for expired long-term tokens
- Pyramid 2.0 compat module as single source of truth for moved imports
- Public API contract tests (tests/test_public_api.py)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add RST stubs for auth, authentication, compat, config, events, mfa,
  models, policy, rate_limit, tokens, views
- Fix duplicate object warnings with :no-index: on re-export modules
- Fix docstring indentation for RST parsing in tokens.py and policy.py
- Add authentication_apis guide to docs toctree

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add narrative getting-started documentation for tet.security covering
setup, token flow, MFA, rate limiting, events, and configuration.

Fix Sphinx autodoc warnings for pyramid_di reify_attr descriptors by
adding an autodoc-process-signature hook that renders them as typed
attributes instead of methods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Bump version to 0.6a1
- Add CHANGES.md entry for the security module
- Fix leeway field leaking into JWT payload (to_dict excluded it)
- Remove phantom structlog dependency from [auth] extras
- Fix qrcode[pil] -> qrcode (we use SVG, not PIL)
- Add missing exports to tet.security.__init__ (TetRateLimitService,
  RateLimitAttemptMixin, TOTPUsedCodeMixin)
- Remove Python 3.8/3.9 classifiers, add python_requires>=3.10
- Fix docs: method_type case (TOTP -> totp), change password field
  names (camelCase), MFA verify payload, list methods response key
- Fix Union[TOTPData] -> TOTPData
- Fix deprecated utcnow() in JWTRegisteredClaims docstring
- Fix bcrypt -> passlib in security guide

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Bump version to 0.6a1 in pyproject.toml (replaces setup.py)
- Add [auth] extras (pyjwt, pyotp, qrcode, requests)
- Set python_requires>=3.10, update tool targets
- Restore autodoc-process-signature hook for pyramid_di descriptors
- Fix authorization tests: providedBy works, so wrapping happens

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ztane added 3 commits June 16, 2026 00:58
The tet.security modules require pyjwt and pyotp; expose them as a 'security'
optional extra and have dev/test pull it in (CI installs .[dev]). Add the
test-only deps the security suite needs: webtest, structlog, psycopg2-binary
(tests run against PostgreSQL). Fixes 'No module named jwt' CI failures.
…nfigurable

The security DB tests need pyramid_tm and zope.sqlalchemy (transaction-managed
sessions) in addition to pyjwt/pyotp; add them to the security extra. Make the
test DB URL overridable via TET_TEST_DB_URL (default unchanged: localhost:5432
for CI) so local runs don't collide with a host postgres on 5432.
Add 3.15-dev to the test matrix with allow-prereleases, marked
continue-on-error so the unreleased nightly doesn't block the build.
Comment thread src/tet/security/auth.py Dismissed
ztane added 3 commits June 18, 2026 15:53
Removes the black dev dependency and [tool.black] config block. Formatting
is handled by ruff-format.

Claude-Session: https://claude.ai/code/session_011KKd5BtHMWfrF9WRRMwg7F
is_password_breached swallowed the RequestException and logged a bare
"unavailable" line with no detail. Add exc_info=True so the actual error
(timeout, DNS, HTTP status) is captured. Behaviour is unchanged: a failed
check still degrades gracefully to False (see
test_breach_api_timeout_graceful_degradation).

Claude-Session: https://claude.ai/code/session_011KKd5BtHMWfrF9WRRMwg7F
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants