jafaal 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. jafaal-0.1.0/.gitignore +32 -0
  2. jafaal-0.1.0/CHANGELOG.md +98 -0
  3. jafaal-0.1.0/LICENSE.md +7 -0
  4. jafaal-0.1.0/PKG-INFO +427 -0
  5. jafaal-0.1.0/README.md +371 -0
  6. jafaal-0.1.0/SECURITY.md +81 -0
  7. jafaal-0.1.0/examples/README.md +103 -0
  8. jafaal-0.1.0/jafaal/__init__.py +453 -0
  9. jafaal-0.1.0/jafaal/_core/__init__.py +18 -0
  10. jafaal-0.1.0/jafaal/_core/crypto.py +94 -0
  11. jafaal-0.1.0/jafaal/_core/db_errors.py +103 -0
  12. jafaal-0.1.0/jafaal/_core/hashing.py +22 -0
  13. jafaal-0.1.0/jafaal/_core/jwk_keys.py +94 -0
  14. jafaal-0.1.0/jafaal/_core/network.py +719 -0
  15. jafaal-0.1.0/jafaal/_core/optional_deps.py +52 -0
  16. jafaal-0.1.0/jafaal/_core/redirect_uris.py +110 -0
  17. jafaal-0.1.0/jafaal/_core/registry.py +87 -0
  18. jafaal-0.1.0/jafaal/_core/timeutils.py +32 -0
  19. jafaal-0.1.0/jafaal/_core/validation.py +18 -0
  20. jafaal-0.1.0/jafaal/_internal/__init__.py +13 -0
  21. jafaal-0.1.0/jafaal/_internal/internal_dependencies.py +693 -0
  22. jafaal-0.1.0/jafaal/_internal/oauth_requests.py +140 -0
  23. jafaal-0.1.0/jafaal/_internal/password_hasher.py +395 -0
  24. jafaal-0.1.0/jafaal/_internal/security_stores.py +934 -0
  25. jafaal-0.1.0/jafaal/_internal/services/__init__.py +1 -0
  26. jafaal-0.1.0/jafaal/_internal/services/account_security_service.py +225 -0
  27. jafaal-0.1.0/jafaal/_internal/services/authorization_code_service.py +580 -0
  28. jafaal-0.1.0/jafaal/_internal/services/credential_sweep.py +119 -0
  29. jafaal-0.1.0/jafaal/_internal/services/identity_link_service.py +290 -0
  30. jafaal-0.1.0/jafaal/_internal/services/mfa_workflow.py +270 -0
  31. jafaal-0.1.0/jafaal/_internal/services/step_up_service.py +279 -0
  32. jafaal-0.1.0/jafaal/_internal/services/token_admin_service.py +251 -0
  33. jafaal-0.1.0/jafaal/_internal/token_denylist.py +107 -0
  34. jafaal-0.1.0/jafaal/_internal/token_manager.py +669 -0
  35. jafaal-0.1.0/jafaal/_internal/user_guards.py +115 -0
  36. jafaal-0.1.0/jafaal/adapters/__init__.py +74 -0
  37. jafaal-0.1.0/jafaal/adapters/event_sinks.py +184 -0
  38. jafaal-0.1.0/jafaal/adapters/password_breach.py +163 -0
  39. jafaal-0.1.0/jafaal/adapters/rate_limiter.py +223 -0
  40. jafaal-0.1.0/jafaal/adapters/redis_state_store.py +190 -0
  41. jafaal-0.1.0/jafaal/adapters/sqlalchemy_user_repository.py +116 -0
  42. jafaal-0.1.0/jafaal/adapters/static_settings.py +68 -0
  43. jafaal-0.1.0/jafaal/api_keys/__init__.py +50 -0
  44. jafaal-0.1.0/jafaal/api_keys/crud.py +376 -0
  45. jafaal-0.1.0/jafaal/api_keys/models.py +99 -0
  46. jafaal-0.1.0/jafaal/api_keys/router.py +200 -0
  47. jafaal-0.1.0/jafaal/api_keys/schema.py +160 -0
  48. jafaal-0.1.0/jafaal/api_keys/utils.py +210 -0
  49. jafaal-0.1.0/jafaal/audit.py +211 -0
  50. jafaal-0.1.0/jafaal/credentials/__init__.py +8 -0
  51. jafaal-0.1.0/jafaal/credentials/crud.py +122 -0
  52. jafaal-0.1.0/jafaal/credentials/models.py +73 -0
  53. jafaal-0.1.0/jafaal/dependencies.py +173 -0
  54. jafaal-0.1.0/jafaal/error_handler.py +77 -0
  55. jafaal-0.1.0/jafaal/exceptions.py +512 -0
  56. jafaal-0.1.0/jafaal/factory.py +439 -0
  57. jafaal-0.1.0/jafaal/identity_providers/__init__.py +86 -0
  58. jafaal-0.1.0/jafaal/identity_providers/crud.py +265 -0
  59. jafaal-0.1.0/jafaal/identity_providers/dependencies.py +19 -0
  60. jafaal-0.1.0/jafaal/identity_providers/discovery.py +704 -0
  61. jafaal-0.1.0/jafaal/identity_providers/id_token.py +234 -0
  62. jafaal-0.1.0/jafaal/identity_providers/link_tokens/__init__.py +50 -0
  63. jafaal-0.1.0/jafaal/identity_providers/link_tokens/crud.py +130 -0
  64. jafaal-0.1.0/jafaal/identity_providers/link_tokens/models.py +103 -0
  65. jafaal-0.1.0/jafaal/identity_providers/link_tokens/schema.py +93 -0
  66. jafaal-0.1.0/jafaal/identity_providers/link_tokens/utils.py +120 -0
  67. jafaal-0.1.0/jafaal/identity_providers/links/__init__.py +73 -0
  68. jafaal-0.1.0/jafaal/identity_providers/links/crud.py +407 -0
  69. jafaal-0.1.0/jafaal/identity_providers/links/models.py +98 -0
  70. jafaal-0.1.0/jafaal/identity_providers/links/schema.py +144 -0
  71. jafaal-0.1.0/jafaal/identity_providers/links/utils.py +100 -0
  72. jafaal-0.1.0/jafaal/identity_providers/models.py +178 -0
  73. jafaal-0.1.0/jafaal/identity_providers/public_router.py +560 -0
  74. jafaal-0.1.0/jafaal/identity_providers/router.py +269 -0
  75. jafaal-0.1.0/jafaal/identity_providers/schema.py +252 -0
  76. jafaal-0.1.0/jafaal/identity_providers/service.py +2065 -0
  77. jafaal-0.1.0/jafaal/identity_providers/utils.py +541 -0
  78. jafaal-0.1.0/jafaal/identity_service.py +1153 -0
  79. jafaal-0.1.0/jafaal/jwks.py +71 -0
  80. jafaal-0.1.0/jafaal/maintenance.py +286 -0
  81. jafaal-0.1.0/jafaal/metadata.py +263 -0
  82. jafaal-0.1.0/jafaal/mfa/__init__.py +18 -0
  83. jafaal-0.1.0/jafaal/mfa/backup_codes/__init__.py +43 -0
  84. jafaal-0.1.0/jafaal/mfa/backup_codes/crud.py +171 -0
  85. jafaal-0.1.0/jafaal/mfa/backup_codes/models.py +87 -0
  86. jafaal-0.1.0/jafaal/mfa/backup_codes/schema.py +39 -0
  87. jafaal-0.1.0/jafaal/mfa/backup_codes/utils.py +87 -0
  88. jafaal-0.1.0/jafaal/mfa/crud.py +97 -0
  89. jafaal-0.1.0/jafaal/mfa/models.py +77 -0
  90. jafaal-0.1.0/jafaal/mfa/schema.py +182 -0
  91. jafaal-0.1.0/jafaal/mfa/service.py +460 -0
  92. jafaal-0.1.0/jafaal/mfa/setup_store.py +274 -0
  93. jafaal-0.1.0/jafaal/migrations/__init__.py +169 -0
  94. jafaal-0.1.0/jafaal/migrations/env.py +66 -0
  95. jafaal-0.1.0/jafaal/migrations/script.py.mako +29 -0
  96. jafaal-0.1.0/jafaal/migrations/versions/rev0001_initial.py +43 -0
  97. jafaal-0.1.0/jafaal/migrations/versions/rev0002_webauthn_credentials.py +43 -0
  98. jafaal-0.1.0/jafaal/migrations/versions/rev0003_oauth_state_upstream_pkce.py +58 -0
  99. jafaal-0.1.0/jafaal/migrations/versions/rev0004_oauth_authorization_code.py +109 -0
  100. jafaal-0.1.0/jafaal/migrations/versions/rev0005_drop_oauth_client_type.py +95 -0
  101. jafaal-0.1.0/jafaal/migrations/versions/rev0006_oauth_requested_scope.py +53 -0
  102. jafaal-0.1.0/jafaal/migrations/versions/rev0007_credential_must_change.py +63 -0
  103. jafaal-0.1.0/jafaal/oauth_state/__init__.py +56 -0
  104. jafaal-0.1.0/jafaal/oauth_state/crud.py +372 -0
  105. jafaal-0.1.0/jafaal/oauth_state/models.py +185 -0
  106. jafaal-0.1.0/jafaal/oauth_state/schema.py +65 -0
  107. jafaal-0.1.0/jafaal/oauth_state/utils.py +39 -0
  108. jafaal-0.1.0/jafaal/orm.py +577 -0
  109. jafaal-0.1.0/jafaal/password_policy.py +42 -0
  110. jafaal-0.1.0/jafaal/password_reset_tokens/__init__.py +50 -0
  111. jafaal-0.1.0/jafaal/password_reset_tokens/crud.py +184 -0
  112. jafaal-0.1.0/jafaal/password_reset_tokens/models.py +63 -0
  113. jafaal-0.1.0/jafaal/password_reset_tokens/router.py +98 -0
  114. jafaal-0.1.0/jafaal/password_reset_tokens/schema.py +112 -0
  115. jafaal-0.1.0/jafaal/password_reset_tokens/utils.py +210 -0
  116. jafaal-0.1.0/jafaal/ports.py +884 -0
  117. jafaal-0.1.0/jafaal/principal.py +187 -0
  118. jafaal-0.1.0/jafaal/py.typed +0 -0
  119. jafaal-0.1.0/jafaal/rate_limit.py +149 -0
  120. jafaal-0.1.0/jafaal/router.py +1720 -0
  121. jafaal-0.1.0/jafaal/schema.py +342 -0
  122. jafaal-0.1.0/jafaal/scopes.py +185 -0
  123. jafaal-0.1.0/jafaal/sessions/__init__.py +66 -0
  124. jafaal-0.1.0/jafaal/sessions/crud.py +583 -0
  125. jafaal-0.1.0/jafaal/sessions/models.py +163 -0
  126. jafaal-0.1.0/jafaal/sessions/rotated_refresh_tokens/__init__.py +35 -0
  127. jafaal-0.1.0/jafaal/sessions/rotated_refresh_tokens/crud.py +151 -0
  128. jafaal-0.1.0/jafaal/sessions/rotated_refresh_tokens/models.py +83 -0
  129. jafaal-0.1.0/jafaal/sessions/rotated_refresh_tokens/schema.py +82 -0
  130. jafaal-0.1.0/jafaal/sessions/rotated_refresh_tokens/utils.py +329 -0
  131. jafaal-0.1.0/jafaal/sessions/router.py +152 -0
  132. jafaal-0.1.0/jafaal/sessions/schema.py +113 -0
  133. jafaal-0.1.0/jafaal/sessions/utils.py +536 -0
  134. jafaal-0.1.0/jafaal/settings.py +1118 -0
  135. jafaal-0.1.0/jafaal/sign_up_tokens/__init__.py +42 -0
  136. jafaal-0.1.0/jafaal/sign_up_tokens/crud.py +155 -0
  137. jafaal-0.1.0/jafaal/sign_up_tokens/models.py +63 -0
  138. jafaal-0.1.0/jafaal/sign_up_tokens/router.py +145 -0
  139. jafaal-0.1.0/jafaal/sign_up_tokens/schema.py +98 -0
  140. jafaal-0.1.0/jafaal/sign_up_tokens/utils.py +271 -0
  141. jafaal-0.1.0/jafaal/state_store.py +286 -0
  142. jafaal-0.1.0/jafaal/token_hashing.py +235 -0
  143. jafaal-0.1.0/jafaal/user_model.py +234 -0
  144. jafaal-0.1.0/jafaal/utils.py +463 -0
  145. jafaal-0.1.0/jafaal/webauthn/__init__.py +8 -0
  146. jafaal-0.1.0/jafaal/webauthn/challenge_store.py +123 -0
  147. jafaal-0.1.0/jafaal/webauthn/crud.py +131 -0
  148. jafaal-0.1.0/jafaal/webauthn/models.py +94 -0
  149. jafaal-0.1.0/jafaal/webauthn/router.py +439 -0
  150. jafaal-0.1.0/jafaal/webauthn/schema.py +156 -0
  151. jafaal-0.1.0/jafaal/webauthn/service.py +420 -0
  152. jafaal-0.1.0/pyproject.toml +416 -0
@@ -0,0 +1,32 @@
1
+ # OS
2
+ .DS_Store
3
+ Thumbs.db
4
+ .vscode/
5
+ .env
6
+ .venv/
7
+
8
+ # Python
9
+ __pycache__/
10
+ .ruff_cache/
11
+ .hypothesis/
12
+ .coverage
13
+ htmlcov/
14
+ coverage.xml
15
+ .pytest_cache/
16
+ *.log
17
+ .mypy_cache/
18
+
19
+ # local dev docs
20
+ devdocs/
21
+
22
+ # built documentation site (mkdocs)
23
+ site/
24
+
25
+ #local dev scripts
26
+ devscripts/
27
+ # build artifacts
28
+ dist/
29
+ *.egg-info/
30
+
31
+ # example app scratch database
32
+ examples/**/*.db
@@ -0,0 +1,98 @@
1
+ # Changelog
2
+
3
+ All notable changes to JAFAAL are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ What counts as a breaking change, and what does not, is defined in [API stability](docs/api-stability.md). During the pre-1.0 series, its public surface is a compatibility target rather than a frozen guarantee: breaking changes may ship in a minor release and are documented here. The full major-version guarantee begins with 1.0.0.
8
+
9
+ ## [0.1.0]
10
+
11
+ Initial pre-1.0 release. The documented public surface is available for integration feedback and may still change between v0.x minor releases. The 1.0.0 compatibility guarantees in [API stability](docs/api-stability.md) remain future-facing until the surface has been validated with production consumers.
12
+
13
+ ### Added
14
+
15
+ **Authentication**
16
+
17
+ - Username/password login with Argon2id hashing, transparent cost upgrades, and no password truncation.
18
+ - NFC normalization before password policy, breach screening, hashing, and verification. Canonically equivalent spellings interoperate, while compatibility characters remain distinct. A separate NFKC form is checked only as a blocklist alias.
19
+ - A default `length_only` policy with a 15-character minimum for regular users and 20 for administrators. Hosts that require composition rules can select `password_type="strict"`.
20
+ - A shared password input limit controlled by `PasswordSettings.max_length`.
21
+ - Breached-password screening through the Have I Been Pwned range API or a host-supplied offline blocklist. Deployed `length_only` configurations require a checker unless `allow_no_password_breach_check_when_deployed=True` is set. The HIBP adapter fails open during service outages.
22
+ - Progressive login lockout by account and source IP, including trusted-proxy handling, bounded password input, and timing-equalized authentication failures.
23
+ - Local sign-up, optional email verification, optional administrator approval, and enumeration-safe sign-up and password-reset responses. User and credential creation share one transaction.
24
+ - Login requires both `is_active` and `is_verified`.
25
+ - `POST /auth/password/change`, `POST /auth/password/renew`, and `POST /auth/password/user/{user_id}` for self-service changes, required replacements, and administrator resets.
26
+ - `jafaal.set_password()` and `jafaal.clear_password()` for trusted host tools such as administrator bootstrap, support workflows, and account imports. Callers remain responsible for authorization.
27
+ - Optional forced password replacement through `must_change=True` and the `password_change_required` error.
28
+ - Password changes and resets revoke affected sessions, API keys, outstanding reset tokens, and pending authentication grants. Self-service changes preserve the caller's current session by default.
29
+ - Non-blocking authentication events for password reset, email verification, account approval, lockout, new-device login, and other security-sensitive changes.
30
+
31
+ **Tokens and sessions**
32
+
33
+ - Authorization-code flow for registered first-party public clients, with mandatory PKCE, single-use codes, exact client and redirect bindings, and local or upstream identity-provider authentication.
34
+ - Browser authorization handoff through `login_ui_url` and an `auth_request` handle, including MFA and passkey completion without exposing tokens to the login page.
35
+ - Redirect URIs limited to complete HTTPS URLs, IP-literal HTTP loopback URLs, and reverse-domain private-use schemes. Loopback ports may vary; other components match exactly.
36
+ - Private-use redirect registrations must use single-slash syntax such as `com.example.app:/callback` instead of authority syntax such as `com.example.app://callback`.
37
+ - Deployed `base_url` and issuer values require absolute HTTPS URLs. Local HTTP development is limited to `127.0.0.1` and `[::1]`.
38
+ - Authorization-server metadata at the issuer-derived location, with a compatibility route under the aggregate router. Mounted routes, external issuers, and trusted ASGI `root_path` values are reflected in advertised endpoints.
39
+ - OAuth authorization, token, introspection, and revocation endpoints return OAuth error objects for missing, malformed, non-text, or repeated parameters instead of FastAPI validation bodies.
40
+ - RFC 9068 JWT access tokens with validated `typ`, `token_use`, and non-empty `client_id` claims.
41
+ - HS256 signing by default, plus RS, PS, and ES algorithms with JWKS publication for asymmetric keys. Symmetric deployments omit `jwks_uri` and return `404` from the JWKS route.
42
+ - Configurable clock-skew handling for token and identity-provider time claims.
43
+ - Requested scopes can narrow a grant and cannot be widened during MFA, passkey completion, authorization-code exchange, or refresh.
44
+ - Client registration controls refresh-token delivery (`body` or `cookie`) and the maximum allowed scope.
45
+ - `/auth/token` supports authorization-code and refresh-token grants. `/auth/refresh` additionally accepts refresh tokens from an `HttpOnly` cookie or `Authorization` header.
46
+ - Refresh-token rotation, bounded retry handling, family-wide reuse detection, and a `stale_refresh_token` response for losing concurrent rotations.
47
+ - Bounded absolute session lifetime, optional idle timeout, CSRF-bound browser sessions, and stable device metadata.
48
+ - Zero-downtime signing-key and encryption-key rotation through configured fallback keys.
49
+ - Token responses and credential-handling endpoints set `Cache-Control: no-store` and `Pragma: no-cache`.
50
+ - Token introspection and client-bound revocation. Valid access-token revocation returns `unsupported_token_type` when access-token denylisting is disabled.
51
+ - RFC 6750 bearer challenges for missing, invalid, expired, or insufficiently scoped credentials. Refresh failures use `400 invalid_grant`.
52
+
53
+ **Multi-factor**
54
+
55
+ - TOTP enrollment and verification with QR provisioning, single-use backup codes, and replay protection. Pending MFA logins remain bound to their original client and requested scope.
56
+ - WebAuthn passkey registration, passwordless authentication, and optional use as a second factor. Passkey registration and deletion require step-up authentication.
57
+ - Enumeration-resistant passwordless discovery using decoy credential identifiers for unknown or passkey-less users.
58
+ - `passkey_login_satisfies_mfa` controls whether a verified passwordless passkey can complete login for an account that also has TOTP enabled.
59
+ - `AuthenticatorChanged` events for TOTP, backup-code, and passkey changes.
60
+ - Step-up authentication for sensitive operations, including fresh upstream authentication for SSO-only accounts.
61
+
62
+ **Identity providers**
63
+
64
+ - OpenID Connect login and account linking with discovery, PKCE, userinfo retrieval, and ID-token signature and claim validation.
65
+ - Validation of `iss`, `aud`, `exp`, `iat`, `nonce`, `azp`, `at_hash`, and userinfo `sub`, with configurable clock-skew allowance.
66
+ - Provider issuer verification during discovery and callback processing, including support for providers that omit `kid` and automatic JWKS refresh during key rotation.
67
+ - Provider authorization errors returned to the registered client with bounded descriptions and normalized error codes.
68
+ - Asymmetric provider JWKS validation. Symmetric verification keys are rejected.
69
+ - Optional email-based account linking through `allow_email_linking`; the provider must assert `email_verified`. New links emit an `IdpAccountLinked` event.
70
+ - Profile synchronization that passes verified email addresses and host-selected profile claims to `UserRepository.sync_from_idp`.
71
+ - Outbound request safeguards including HTTPS enforcement, public-address resolution, pinned connections, redirect refusal for credential-bearing requests, timeouts, and response-size limits.
72
+
73
+ **Authorization and integration**
74
+
75
+ - API keys with host-configured scope allow-lists, optional expiry, revocation, deletion, and immediate scope narrowing after account-role changes.
76
+ - `reauthorize_scopes_per_request` for applying current account permissions to access tokens before expiry.
77
+ - An extensible scope catalog with descriptions for OpenAPI authorization controls. Scope denials include an `insufficient_scope` bearer challenge.
78
+ - Host-provided ports for users, dynamic settings, event delivery, password breach checks, rate limiting, scope resolution, and shared state.
79
+ - Built-in SQLAlchemy user, static settings, logging event, HIBP and blocklist, state-store rate-limit, and Redis state adapters.
80
+ - Caller-owned SQLAlchemy transactions. Repository and service operations flush without committing, and `jafaal.unit_of_work()` provides an optional transaction boundary.
81
+ - Structured security events on the `jafaal.audit` logger, with optional PII scrubbing.
82
+ - Bounded, non-blocking `AuthEventSink` delivery with reserved capacity for security-critical events.
83
+ - Stable domain error codes and centralized FastAPI exception handling.
84
+ - Alembic migrations for JAFAAL companion tables, using a dedicated version table alongside the host application's migration history.
85
+
86
+ **Packaging**
87
+
88
+ - Typed package support for Python 3.12 and later, tested through Python 3.14.
89
+ - SQLite, PostgreSQL, and MySQL support, plus in-memory and Redis state stores.
90
+ - Optional extras for `mfa`, `sso`, `webauthn`, `redis`, and `migrations`. Missing extras fail with an installation hint when the feature is used.
91
+ - Direct dependency declarations for imported runtime packages, including Starlette.
92
+ - Startup validation for required host adapters and deployed-environment safeguards. `verify=False` can disable router startup verification.
93
+ - Deployed environments require a configured rate limiter and distributed state store. Environment names are validated, including `staging`, `production`, and `demo`.
94
+ - A startup warning when HS256 is used with registered OAuth clients; asymmetric signing is recommended for independently deployed resource servers.
95
+
96
+ See [Security](https://jafaal.endurain.com/security/) and [Threat model](https://jafaal.endurain.com/threat-model/) for the security design and the host's responsibilities.
97
+
98
+ [0.1.0]: https://github.com/endurain-project/jafaal/releases/tag/v0.1.0
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2025 João Vitória Silva
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
jafaal-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,427 @@
1
+ Metadata-Version: 2.4
2
+ Name: jafaal
3
+ Version: 0.1.0
4
+ Summary: Just Another FastAPI Authentication Library
5
+ Project-URL: Homepage, https://jafaal.endurain.com/
6
+ Project-URL: Documentation, https://jafaal.endurain.com/
7
+ Project-URL: Repository, https://github.com/endurain-project/jafaal
8
+ Project-URL: Changelog, https://github.com/endurain-project/jafaal/blob/main/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/endurain-project/jafaal/issues
10
+ Project-URL: Security, https://github.com/endurain-project/jafaal/blob/main/SECURITY.md
11
+ Author-email: João Vitória Silva <joao@endurain.com>
12
+ License: MIT
13
+ License-File: LICENSE.md
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Framework :: FastAPI
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Security
24
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: <4.0,>=3.12
27
+ Requires-Dist: cryptography>=50.0.0
28
+ Requires-Dist: fastapi>=0.136.0
29
+ Requires-Dist: httpx>=0.28.1
30
+ Requires-Dist: joserfc>=1.6.4
31
+ Requires-Dist: pwdlib[argon2]>=0.3.0
32
+ Requires-Dist: pydantic[email]>=2.12.5
33
+ Requires-Dist: python-multipart>=0.0.9
34
+ Requires-Dist: sqlalchemy>=2.0.49
35
+ Requires-Dist: starlette>=0.46.0
36
+ Requires-Dist: user-agents>=2.2.0
37
+ Provides-Extra: all
38
+ Requires-Dist: alembic>=1.13; extra == 'all'
39
+ Requires-Dist: authlib>=1.6.11; extra == 'all'
40
+ Requires-Dist: pyotp>=2.9.0; extra == 'all'
41
+ Requires-Dist: qrcode[pil]>=8.2; extra == 'all'
42
+ Requires-Dist: redis>=5.0.0; extra == 'all'
43
+ Requires-Dist: webauthn>=2.2.0; extra == 'all'
44
+ Provides-Extra: mfa
45
+ Requires-Dist: pyotp>=2.9.0; extra == 'mfa'
46
+ Requires-Dist: qrcode[pil]>=8.2; extra == 'mfa'
47
+ Provides-Extra: migrations
48
+ Requires-Dist: alembic>=1.13; extra == 'migrations'
49
+ Provides-Extra: redis
50
+ Requires-Dist: redis>=5.0.0; extra == 'redis'
51
+ Provides-Extra: sso
52
+ Requires-Dist: authlib>=1.6.11; extra == 'sso'
53
+ Provides-Extra: webauthn
54
+ Requires-Dist: webauthn>=2.2.0; extra == 'webauthn'
55
+ Description-Content-Type: text/markdown
56
+
57
+ # Just Another FastAPI Authentication Library (JAFAAL)
58
+
59
+ [![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/endurain-project/jafaal/blob/main/LICENSE.md)
60
+ [![Release](https://img.shields.io/badge/dynamic/json?url=https://api.github.com/repos/endurain-project/jafaal/releases/latest&query=$.tag_name&label=release&color=blue)](https://github.com/endurain-project/jafaal/releases)
61
+ [![PyPI version](https://img.shields.io/pypi/v/jafaal)](https://pypi.org/project/jafaal/)
62
+ [![PyPI downloads](https://img.shields.io/pypi/dm/jafaal)](https://pypi.org/project/jafaal/)
63
+ [![Python](https://img.shields.io/badge/python-3.12%2B-blue)](https://pypi.org/project/jafaal/)
64
+ [![Docs](https://img.shields.io/badge/docs-jafaal.endurain.com-blue)](https://jafaal.endurain.com/)
65
+ [![Stars](https://img.shields.io/badge/dynamic/json?url=https://api.github.com/repos/endurain-project/jafaal&query=$.stars_count&label=stars&logo=github)](https://github.com/endurain-project/jafaal)
66
+
67
+ ## What is JAFAAL?
68
+
69
+ JAFAAL is a batteries-included, embedded FastAPI authentication library and a
70
+ standards-shaped authorization server for applications controlled by one host.
71
+ It integrates with synchronous SQLAlchemy and owns the security-critical parts
72
+ of auth so your app doesn't have to:
73
+
74
+ - **Password login** (Argon2id) with progressive per-account lockout
75
+ - **JWT access/refresh tokens** (HS256) with refresh-token **rotation + reuse detection**
76
+ - **Sessions** with idle/absolute timeout, CSRF binding, and web/mobile (PKCE) flows
77
+ - **MFA** (TOTP + single-use backup codes) with replay protection
78
+ - **API keys** with a host-controlled scope allow-list
79
+ - **SSO / OIDC** identity providers with SSRF-guarded outbound calls
80
+ - **Password reset & sign-up** flows that emit events (you deliver the email)
81
+ - A **`JafaalError` → HTTP** edge handler, so the core never imports HTTP concerns
82
+
83
+ JAFAAL depends only on a small set of **ports** you implement (your user table,
84
+ your dynamic settings, and how you deliver notifications). Everything else —
85
+ tables, routers, token logic — ships with the library.
86
+
87
+ ## What JAFAAL is (and is not)
88
+
89
+ JAFAAL authenticates **your** users for **your** API. Concretely, it plays four
90
+ roles, and implements the standards that govern each:
91
+
92
+ | Role | Standards |
93
+ |---|---|
94
+ | **JWT issuer** for your own resource servers | RFC 9068 (`at+jwt` access tokens), RFC 7519, RFC 7517 / 7638 (JWKS + `kid` thumbprints), RFC 8414 (discovery) |
95
+ | **Bearer-token resource server** | RFC 6750 (header extraction, `WWW-Authenticate` challenges incl. `insufficient_scope`) |
96
+ | **Authorization server for your own native apps** | RFC 6749 §4.1 (authorization code), RFC 7636 (PKCE S256), RFC 8252 (native apps / public clients), RFC 9700 (exact redirect-URI matching) |
97
+ | **OAuth client / OIDC Relying Party** for SSO | RFC 6749 *client* role, RFC 7636, OIDC Core 1.0 (`nonce`, `azp`, `at_hash`, userinfo `sub` check), RFC 9700 |
98
+ | **Credential authority** | NIST SP 800-63B-4 (NFC + length policy + configured blocklist), RFC 6238 (TOTP), W3C WebAuthn L2 (passkeys), RFC 7662 (introspection), RFC 7009 (revocation) |
99
+
100
+ NIST password alignment is conditional on a configured checker being available.
101
+ The HIBP adapter fails open during an outage; use a local fail-closed blocklist
102
+ when uninterrupted enforcement is required.
103
+
104
+ > [!IMPORTANT]
105
+ > **JAFAAL is an authorization server for clients you own.**
106
+ >
107
+ > That is the boundary, and it is deliberate rather than unfinished. Register
108
+ > your applications via `AuthSettings.oauth_clients` and drive `/auth/authorize`
109
+ > → `/auth/token` with any standard OAuth client library — PKCE mandatory,
110
+ > `code` the only response type, redirect URIs matched byte-for-byte except for
111
+ > native IP-loopback ports, clients public per RFC 8252.
112
+ >
113
+ > **Not planned:** third-party clients (consent screen, client secrets, dynamic
114
+ > registration), being an OpenID Provider (`id_token`, userinfo, the logout
115
+ > specs, certification), `client_credentials`, and the implicit/hybrid/ROPC
116
+ > grants that OAuth 2.1 removes. If you need to *be* an identity provider, put a
117
+ > real one in front of JAFAAL — it already speaks to Keycloak, Authentik,
118
+ > Authelia, Casdoor and Pocket ID as a relying party.
119
+ >
120
+ > `POST /auth/login` authenticates a first-party user directly; it is **not** the
121
+ > resource-owner password-credentials grant, and the discovery document
122
+ > deliberately does not advertise it. A native app should prefer
123
+ > `/auth/authorize`, which keeps the password out of the app entirely
124
+ > (RFC 8252 §8.1).
125
+
126
+ Both **web and mobile** clients are first-class. They differ only in refresh-token
127
+ delivery: browsers get an `HttpOnly`, `SameSite=Strict` cookie (so page script
128
+ never touches it, per RFC 9700 §7.2), native clients get it in the response body.
129
+
130
+ ## Current limitations
131
+
132
+ - **Synchronous SQLAlchemy only.** JAFAAL's endpoints and CRUD layer take a
133
+ `Session`, not an `AsyncSession`, and the registered factory must be a sync
134
+ `sessionmaker`. FastAPI runs synchronous handlers in its worker thread pool;
135
+ JAFAAL's writes cannot share a transaction with host `AsyncSession` work.
136
+ - **One process-wide configuration.** `jafaal.configure()` and the
137
+ `configure_*` adapter functions install module-level settings, ports, stores,
138
+ and registries shared by every JAFAAL router in the process. Two differently
139
+ configured JAFAAL instances cannot be isolated in one process. Each worker
140
+ must configure itself, and replicas need distributed state where documented.
141
+ - **First-party public clients only.** OAuth clients are trusted applications
142
+ owned by the same host, registered statically in `AuthSettings.oauth_clients`,
143
+ and authenticated with PKCE rather than a client secret. JAFAAL v0.1 has no
144
+ third-party client lifecycle, consent records, confidential-client
145
+ authentication, or dynamic registration.
146
+
147
+ ## Installation
148
+
149
+ ```bash
150
+ pip install jafaal
151
+ # or
152
+ uv add jafaal
153
+ ```
154
+
155
+ Requires Python 3.12+.
156
+
157
+ ### Optional features
158
+
159
+ A minimal "login + JWT + sessions" deployment needs no extras. Multi-factor
160
+ authentication and single sign-on pull in additional packages, so they ship as
161
+ optional extras. Install only what you use:
162
+
163
+ ```bash
164
+ pip install 'jafaal[mfa]' # TOTP MFA (pyotp) + QR provisioning (qrcode)
165
+ pip install 'jafaal[webauthn]' # passkeys / WebAuthn (py_webauthn)
166
+ pip install 'jafaal[sso]' # OpenID Connect identity providers (authlib)
167
+ pip install 'jafaal[redis]' # distributed StateStore adapter (redis)
168
+ pip install 'jafaal[migrations]' # packaged Alembic revisions
169
+ pip install 'jafaal[all]' # everything
170
+ ```
171
+
172
+ If a feature is used without its extra installed, JAFAAL fails fast with a clear
173
+ install hint (a `MissingDependencyError`) rather than an obscure error.
174
+
175
+ ### Verifying a release
176
+
177
+ Releases are built and published by [this repository's release workflow](.github/workflows/publish-jafaal.yml) through PyPI Trusted Publishing, with [PEP 740](https://peps.python.org/pep-0740/) attestations. You can confirm a downloaded artifact came from that workflow and was not substituted:
178
+
179
+ ```bash
180
+ uvx pypi-attestations verify pypi \
181
+ --repository https://github.com/endurain-project/jafaal \
182
+ pypi:jafaal-<version>-py3-none-any.whl
183
+ ```
184
+
185
+ A successful run prints `OK: <filename>`. `Provenance for file ... was not found` means the artifact predates attested publishing rather than that verification failed.
186
+
187
+ Each release run also produces a CycloneDX SBOM and `SHA256SUMS`, generated from a clean install of the built wheel. These are retained as workflow artifacts on the release run rather than published to PyPI.
188
+
189
+ ## Quickstart
190
+
191
+ ### 1. Configure the library
192
+
193
+ JAFAAL never reads environment variables itself — you build the settings and
194
+ inject them once at startup. Configuration is grouped by concern, so you only
195
+ read the groups you actually use.
196
+
197
+ ```python
198
+ import jafaal
199
+ from cryptography.fernet import Fernet
200
+
201
+ jafaal.configure(
202
+ jafaal.AuthSettings(
203
+ secrets=jafaal.Secrets(
204
+ secret_key="<32+ byte JWT signing secret>",
205
+ fernet_key=Fernet.generate_key().decode(), # at-rest token encryption
206
+ ),
207
+ base_url="https://app.example.com",
208
+ app_name="Example", # shown in authenticator apps
209
+ environment="production", # drives the cookie Secure flag
210
+ # Every other group has working defaults; override only what you need:
211
+ # tokens=jafaal.TokenSettings(access_token_expire_minutes=10),
212
+ # sessions=jafaal.SessionSettings(idle_timeout_enabled=True),
213
+ # webauthn=jafaal.WebAuthnSettings(second_factor_enabled=True),
214
+ )
215
+ )
216
+ ```
217
+
218
+ ### 2. Own your `Base`, map JAFAAL's tables in, and register a session factory
219
+
220
+ You own the declarative registry; JAFAAL maps its companion tables into it with
221
+ `map_models`, so both share one metadata. Your model **must** be named `Users`,
222
+ mapped to the `users` table.
223
+
224
+ ```python
225
+ from sqlalchemy import String, create_engine
226
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker
227
+
228
+ from jafaal import IntPKUserMixin
229
+
230
+
231
+ class Base(DeclarativeBase): # you own the base
232
+ pass
233
+
234
+
235
+ class Users(IntPKUserMixin, Base):
236
+ __tablename__ = "users"
237
+ # Add any app-specific profile columns — JAFAAL never touches them:
238
+ display_name: Mapped[str | None] = mapped_column(String(250))
239
+
240
+
241
+ jafaal.map_models(Base) # map JAFAAL's companion tables into your registry
242
+
243
+ engine = create_engine("postgresql+psycopg://...")
244
+ jafaal.configure_sessionmaker(sessionmaker(bind=engine, autoflush=False))
245
+ Base.metadata.create_all(engine) # or use Alembic migrations
246
+ ```
247
+
248
+ The reverse relationships (`users_sessions`, `local_credential`, `auth_mfa`, …)
249
+ and the `mfa_enabled` property are supplied by the mixin — you declare none of them.
250
+
251
+ ### 3. Implement the ports
252
+
253
+ ```python
254
+ from jafaal import (
255
+ PasswordPolicy,
256
+ SignupConfig,
257
+ UserProtocol,
258
+ configure_settings_provider,
259
+ configure_user_repository,
260
+ )
261
+
262
+
263
+ class SqlUserRepository:
264
+ def get_by_id(self, user_id, db) -> UserProtocol | None:
265
+ return db.get(Users, user_id)
266
+
267
+ def get_by_email(self, email, db):
268
+ return db.query(Users).filter(Users.email == email).one_or_none()
269
+
270
+ def get_by_username(self, username, db):
271
+ return db.query(Users).filter(Users.username == username).one_or_none()
272
+
273
+ def create_local_user(self, username, email, db, *, is_active, is_verified):
274
+ user = Users(username=username, email=email, is_active=is_active, is_verified=is_verified)
275
+ db.add(user)
276
+ db.flush()
277
+ db.refresh(user)
278
+ return user
279
+
280
+ def provision_from_idp(self, identity, db): ... # SSO auto-provisioning
281
+ def sync_from_idp(self, user_id, claims, db): ... # optional profile sync
282
+ def set_email_verified(self, user_id, db, *, activate):
283
+ user = db.get(Users, user_id)
284
+ user.is_verified = True
285
+ if activate:
286
+ user.is_active = True
287
+ db.flush()
288
+
289
+
290
+ class StaticSettingsProvider:
291
+ def get_password_policy(self) -> PasswordPolicy:
292
+ return PasswordPolicy(min_length_regular=15, min_length_admin=20, password_type="length_only")
293
+
294
+ def get_signup_config(self) -> SignupConfig:
295
+ return SignupConfig(enabled=True, require_email_verification=False, require_admin_approval=False)
296
+
297
+
298
+ configure_user_repository(SqlUserRepository())
299
+ configure_settings_provider(StaticSettingsProvider())
300
+ ```
301
+
302
+ Notifications (password-reset / sign-up emails, admin pings) are optional — JAFAAL
303
+ emits events to an `AuthEventSink`; install one with `jafaal.configure_event_sink(...)`
304
+ to deliver them, or skip it and those flows simply mint tokens without sending mail.
305
+
306
+ ### 4. Mount the router, and run maintenance
307
+
308
+ ```python
309
+ import contextlib
310
+
311
+ from fastapi import FastAPI
312
+ import jafaal
313
+
314
+
315
+ @contextlib.asynccontextmanager
316
+ async def lifespan(app: FastAPI):
317
+ # Sweeps consumed OAuth states, rotated refresh tokens and expired
318
+ # reset/sign-up tokens. Without it those tables grow without bound.
319
+ # Already have a scheduler? Call jafaal.maintenance.run_due_tasks() from it.
320
+ jafaal.maintenance.start_background_scheduler()
321
+ yield
322
+ # Closes the pooled OIDC HTTP client, stops the sweeper, drains events.
323
+ await jafaal.shutdown()
324
+
325
+
326
+ app = FastAPI(lifespan=lifespan)
327
+ # Registers the JafaalError→HTTP handler and aggregates every sub-router.
328
+ app.include_router(jafaal.create_auth_router(app=app), prefix="/api/v1")
329
+ ```
330
+
331
+ That's it — you now have `/api/v1/auth/login`, `/refresh`, `/logout`, session
332
+ management, MFA, API keys, SSO, sign-up and password-reset endpoints.
333
+
334
+ ### 5. Transactions: you own the unit of work
335
+
336
+ Every JAFAAL function that takes a `Session` participates in **your**
337
+ transaction and never commits — the CRUD layer only flushes. JAFAAL's own
338
+ endpoints commit exactly once per request; when *you* drive JAFAAL's services,
339
+ you decide the boundary:
340
+
341
+ ```python
342
+ with jafaal.unit_of_work(db):
343
+ user = repo.create_local_user("ada", "ada@example.com", db, is_active=True, is_verified=False)
344
+ identity_service.set_local_password_hash(user.id, hashed)
345
+ db.add(MyProfile(user_id=user.id))
346
+ # one commit — any failure rolls back all three
347
+ ```
348
+
349
+ ### 6. Optional configuration
350
+
351
+ ```python
352
+ from jafaal import DEFAULT_SCOPE_CATALOG, configure_scopes, configure_api_key_scopes
353
+
354
+ # Layer your application scopes on top of JAFAAL's auth/identity scopes:
355
+ configure_scopes(
356
+ DEFAULT_SCOPE_CATALOG.extend(
357
+ regular=("reports:read",),
358
+ admin=("reports:read", "reports:write"),
359
+ descriptions={"reports:read": "Read reports", "reports:write": "Manage reports"},
360
+ )
361
+ )
362
+
363
+ # Opt each scope an API key may carry in explicitly (empty by default):
364
+ configure_api_key_scopes(["reports:read"])
365
+
366
+ # Native apps use the standard RFC 6749 authorization-code flow with PKCE.
367
+ # Register each one so redirect URIs can be matched exactly (RFC 9700 §4.1):
368
+ # jafaal.AuthSettings(..., oauth_clients=(
369
+ # jafaal.OAuthClient(client_id="com.example.app",
370
+ # redirect_uris=("com.example.app:/oauth/callback",)),
371
+ # ))
372
+
373
+ # Richer authorisation than the built-in is_superuser two tiers? Implement the
374
+ # ScopeResolver port and JAFAAL stamps whatever you return into its tokens:
375
+ # jafaal.configure_scope_resolver(MyRoleBasedResolver())
376
+
377
+ # jafaal.configure_rate_limiter(...) # inject a real limiter (e.g. slowapi)
378
+ # jafaal.configure_state_store(...) # inject Redis for multi-worker lockout state
379
+ ```
380
+
381
+ By default JAFAAL runs in a single process with an in-memory state store and no
382
+ rate limiting. For multi-worker/replica deployments, inject a distributed
383
+ `StateStore` and a `RateLimiter`.
384
+
385
+ ## Examples
386
+
387
+ [`examples/`](examples/) has a complete, runnable app — user model, ports and
388
+ router in one file — plus the two client-side walkthroughs it drives:
389
+
390
+ ```bash
391
+ cd examples/minimal_app
392
+ uv run --with 'jafaal[all]' --with uvicorn uvicorn app:app --reload
393
+ ```
394
+
395
+ - [Web client walkthrough](examples/web_client.md) — cookie refresh, CSRF, page
396
+ reload, MFA
397
+ - [Mobile client walkthrough](examples/mobile_client.md) — the authorization-code
398
+ flow with PKCE
399
+
400
+ ## Documentation
401
+
402
+ Full documentation lives at [jafaal.endurain.com](https://jafaal.endurain.com/).
403
+ The [client integration reference](https://jafaal.endurain.com/clients/) is the
404
+ HTTP contract your front end codes against.
405
+
406
+ ## Sponsors
407
+
408
+ A huge thank you to the project sponsors! Your support helps keep this project going.
409
+
410
+ Support Endurain's development on:
411
+
412
+ - [Buy Me a Coffee](https://buymeacoffee.com/endurain)
413
+ - [liberapay](https://liberapay.com/endurain/)
414
+ - [Patreon](https://patreon.com/u84745218)
415
+ - [GitHub Sponsors using archived repo](https://github.com/endurain-project/endurain)
416
+
417
+ ## Contributing
418
+
419
+ Contributions are welcomed! Please open an issue to discuss any changes or improvements before submitting a PR. Check out the [Contributing Guidelines](CONTRIBUTING.md) for more details.
420
+
421
+ ## License
422
+
423
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details.
424
+
425
+ <div align="center">
426
+ <sub>Built with ❤️ from Portugal | Part of the <a href="https://github.com/endurain-project">Endurain</a> ecosystem</sub>
427
+ </div>