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.
- jafaal-0.1.0/.gitignore +32 -0
- jafaal-0.1.0/CHANGELOG.md +98 -0
- jafaal-0.1.0/LICENSE.md +7 -0
- jafaal-0.1.0/PKG-INFO +427 -0
- jafaal-0.1.0/README.md +371 -0
- jafaal-0.1.0/SECURITY.md +81 -0
- jafaal-0.1.0/examples/README.md +103 -0
- jafaal-0.1.0/jafaal/__init__.py +453 -0
- jafaal-0.1.0/jafaal/_core/__init__.py +18 -0
- jafaal-0.1.0/jafaal/_core/crypto.py +94 -0
- jafaal-0.1.0/jafaal/_core/db_errors.py +103 -0
- jafaal-0.1.0/jafaal/_core/hashing.py +22 -0
- jafaal-0.1.0/jafaal/_core/jwk_keys.py +94 -0
- jafaal-0.1.0/jafaal/_core/network.py +719 -0
- jafaal-0.1.0/jafaal/_core/optional_deps.py +52 -0
- jafaal-0.1.0/jafaal/_core/redirect_uris.py +110 -0
- jafaal-0.1.0/jafaal/_core/registry.py +87 -0
- jafaal-0.1.0/jafaal/_core/timeutils.py +32 -0
- jafaal-0.1.0/jafaal/_core/validation.py +18 -0
- jafaal-0.1.0/jafaal/_internal/__init__.py +13 -0
- jafaal-0.1.0/jafaal/_internal/internal_dependencies.py +693 -0
- jafaal-0.1.0/jafaal/_internal/oauth_requests.py +140 -0
- jafaal-0.1.0/jafaal/_internal/password_hasher.py +395 -0
- jafaal-0.1.0/jafaal/_internal/security_stores.py +934 -0
- jafaal-0.1.0/jafaal/_internal/services/__init__.py +1 -0
- jafaal-0.1.0/jafaal/_internal/services/account_security_service.py +225 -0
- jafaal-0.1.0/jafaal/_internal/services/authorization_code_service.py +580 -0
- jafaal-0.1.0/jafaal/_internal/services/credential_sweep.py +119 -0
- jafaal-0.1.0/jafaal/_internal/services/identity_link_service.py +290 -0
- jafaal-0.1.0/jafaal/_internal/services/mfa_workflow.py +270 -0
- jafaal-0.1.0/jafaal/_internal/services/step_up_service.py +279 -0
- jafaal-0.1.0/jafaal/_internal/services/token_admin_service.py +251 -0
- jafaal-0.1.0/jafaal/_internal/token_denylist.py +107 -0
- jafaal-0.1.0/jafaal/_internal/token_manager.py +669 -0
- jafaal-0.1.0/jafaal/_internal/user_guards.py +115 -0
- jafaal-0.1.0/jafaal/adapters/__init__.py +74 -0
- jafaal-0.1.0/jafaal/adapters/event_sinks.py +184 -0
- jafaal-0.1.0/jafaal/adapters/password_breach.py +163 -0
- jafaal-0.1.0/jafaal/adapters/rate_limiter.py +223 -0
- jafaal-0.1.0/jafaal/adapters/redis_state_store.py +190 -0
- jafaal-0.1.0/jafaal/adapters/sqlalchemy_user_repository.py +116 -0
- jafaal-0.1.0/jafaal/adapters/static_settings.py +68 -0
- jafaal-0.1.0/jafaal/api_keys/__init__.py +50 -0
- jafaal-0.1.0/jafaal/api_keys/crud.py +376 -0
- jafaal-0.1.0/jafaal/api_keys/models.py +99 -0
- jafaal-0.1.0/jafaal/api_keys/router.py +200 -0
- jafaal-0.1.0/jafaal/api_keys/schema.py +160 -0
- jafaal-0.1.0/jafaal/api_keys/utils.py +210 -0
- jafaal-0.1.0/jafaal/audit.py +211 -0
- jafaal-0.1.0/jafaal/credentials/__init__.py +8 -0
- jafaal-0.1.0/jafaal/credentials/crud.py +122 -0
- jafaal-0.1.0/jafaal/credentials/models.py +73 -0
- jafaal-0.1.0/jafaal/dependencies.py +173 -0
- jafaal-0.1.0/jafaal/error_handler.py +77 -0
- jafaal-0.1.0/jafaal/exceptions.py +512 -0
- jafaal-0.1.0/jafaal/factory.py +439 -0
- jafaal-0.1.0/jafaal/identity_providers/__init__.py +86 -0
- jafaal-0.1.0/jafaal/identity_providers/crud.py +265 -0
- jafaal-0.1.0/jafaal/identity_providers/dependencies.py +19 -0
- jafaal-0.1.0/jafaal/identity_providers/discovery.py +704 -0
- jafaal-0.1.0/jafaal/identity_providers/id_token.py +234 -0
- jafaal-0.1.0/jafaal/identity_providers/link_tokens/__init__.py +50 -0
- jafaal-0.1.0/jafaal/identity_providers/link_tokens/crud.py +130 -0
- jafaal-0.1.0/jafaal/identity_providers/link_tokens/models.py +103 -0
- jafaal-0.1.0/jafaal/identity_providers/link_tokens/schema.py +93 -0
- jafaal-0.1.0/jafaal/identity_providers/link_tokens/utils.py +120 -0
- jafaal-0.1.0/jafaal/identity_providers/links/__init__.py +73 -0
- jafaal-0.1.0/jafaal/identity_providers/links/crud.py +407 -0
- jafaal-0.1.0/jafaal/identity_providers/links/models.py +98 -0
- jafaal-0.1.0/jafaal/identity_providers/links/schema.py +144 -0
- jafaal-0.1.0/jafaal/identity_providers/links/utils.py +100 -0
- jafaal-0.1.0/jafaal/identity_providers/models.py +178 -0
- jafaal-0.1.0/jafaal/identity_providers/public_router.py +560 -0
- jafaal-0.1.0/jafaal/identity_providers/router.py +269 -0
- jafaal-0.1.0/jafaal/identity_providers/schema.py +252 -0
- jafaal-0.1.0/jafaal/identity_providers/service.py +2065 -0
- jafaal-0.1.0/jafaal/identity_providers/utils.py +541 -0
- jafaal-0.1.0/jafaal/identity_service.py +1153 -0
- jafaal-0.1.0/jafaal/jwks.py +71 -0
- jafaal-0.1.0/jafaal/maintenance.py +286 -0
- jafaal-0.1.0/jafaal/metadata.py +263 -0
- jafaal-0.1.0/jafaal/mfa/__init__.py +18 -0
- jafaal-0.1.0/jafaal/mfa/backup_codes/__init__.py +43 -0
- jafaal-0.1.0/jafaal/mfa/backup_codes/crud.py +171 -0
- jafaal-0.1.0/jafaal/mfa/backup_codes/models.py +87 -0
- jafaal-0.1.0/jafaal/mfa/backup_codes/schema.py +39 -0
- jafaal-0.1.0/jafaal/mfa/backup_codes/utils.py +87 -0
- jafaal-0.1.0/jafaal/mfa/crud.py +97 -0
- jafaal-0.1.0/jafaal/mfa/models.py +77 -0
- jafaal-0.1.0/jafaal/mfa/schema.py +182 -0
- jafaal-0.1.0/jafaal/mfa/service.py +460 -0
- jafaal-0.1.0/jafaal/mfa/setup_store.py +274 -0
- jafaal-0.1.0/jafaal/migrations/__init__.py +169 -0
- jafaal-0.1.0/jafaal/migrations/env.py +66 -0
- jafaal-0.1.0/jafaal/migrations/script.py.mako +29 -0
- jafaal-0.1.0/jafaal/migrations/versions/rev0001_initial.py +43 -0
- jafaal-0.1.0/jafaal/migrations/versions/rev0002_webauthn_credentials.py +43 -0
- jafaal-0.1.0/jafaal/migrations/versions/rev0003_oauth_state_upstream_pkce.py +58 -0
- jafaal-0.1.0/jafaal/migrations/versions/rev0004_oauth_authorization_code.py +109 -0
- jafaal-0.1.0/jafaal/migrations/versions/rev0005_drop_oauth_client_type.py +95 -0
- jafaal-0.1.0/jafaal/migrations/versions/rev0006_oauth_requested_scope.py +53 -0
- jafaal-0.1.0/jafaal/migrations/versions/rev0007_credential_must_change.py +63 -0
- jafaal-0.1.0/jafaal/oauth_state/__init__.py +56 -0
- jafaal-0.1.0/jafaal/oauth_state/crud.py +372 -0
- jafaal-0.1.0/jafaal/oauth_state/models.py +185 -0
- jafaal-0.1.0/jafaal/oauth_state/schema.py +65 -0
- jafaal-0.1.0/jafaal/oauth_state/utils.py +39 -0
- jafaal-0.1.0/jafaal/orm.py +577 -0
- jafaal-0.1.0/jafaal/password_policy.py +42 -0
- jafaal-0.1.0/jafaal/password_reset_tokens/__init__.py +50 -0
- jafaal-0.1.0/jafaal/password_reset_tokens/crud.py +184 -0
- jafaal-0.1.0/jafaal/password_reset_tokens/models.py +63 -0
- jafaal-0.1.0/jafaal/password_reset_tokens/router.py +98 -0
- jafaal-0.1.0/jafaal/password_reset_tokens/schema.py +112 -0
- jafaal-0.1.0/jafaal/password_reset_tokens/utils.py +210 -0
- jafaal-0.1.0/jafaal/ports.py +884 -0
- jafaal-0.1.0/jafaal/principal.py +187 -0
- jafaal-0.1.0/jafaal/py.typed +0 -0
- jafaal-0.1.0/jafaal/rate_limit.py +149 -0
- jafaal-0.1.0/jafaal/router.py +1720 -0
- jafaal-0.1.0/jafaal/schema.py +342 -0
- jafaal-0.1.0/jafaal/scopes.py +185 -0
- jafaal-0.1.0/jafaal/sessions/__init__.py +66 -0
- jafaal-0.1.0/jafaal/sessions/crud.py +583 -0
- jafaal-0.1.0/jafaal/sessions/models.py +163 -0
- jafaal-0.1.0/jafaal/sessions/rotated_refresh_tokens/__init__.py +35 -0
- jafaal-0.1.0/jafaal/sessions/rotated_refresh_tokens/crud.py +151 -0
- jafaal-0.1.0/jafaal/sessions/rotated_refresh_tokens/models.py +83 -0
- jafaal-0.1.0/jafaal/sessions/rotated_refresh_tokens/schema.py +82 -0
- jafaal-0.1.0/jafaal/sessions/rotated_refresh_tokens/utils.py +329 -0
- jafaal-0.1.0/jafaal/sessions/router.py +152 -0
- jafaal-0.1.0/jafaal/sessions/schema.py +113 -0
- jafaal-0.1.0/jafaal/sessions/utils.py +536 -0
- jafaal-0.1.0/jafaal/settings.py +1118 -0
- jafaal-0.1.0/jafaal/sign_up_tokens/__init__.py +42 -0
- jafaal-0.1.0/jafaal/sign_up_tokens/crud.py +155 -0
- jafaal-0.1.0/jafaal/sign_up_tokens/models.py +63 -0
- jafaal-0.1.0/jafaal/sign_up_tokens/router.py +145 -0
- jafaal-0.1.0/jafaal/sign_up_tokens/schema.py +98 -0
- jafaal-0.1.0/jafaal/sign_up_tokens/utils.py +271 -0
- jafaal-0.1.0/jafaal/state_store.py +286 -0
- jafaal-0.1.0/jafaal/token_hashing.py +235 -0
- jafaal-0.1.0/jafaal/user_model.py +234 -0
- jafaal-0.1.0/jafaal/utils.py +463 -0
- jafaal-0.1.0/jafaal/webauthn/__init__.py +8 -0
- jafaal-0.1.0/jafaal/webauthn/challenge_store.py +123 -0
- jafaal-0.1.0/jafaal/webauthn/crud.py +131 -0
- jafaal-0.1.0/jafaal/webauthn/models.py +94 -0
- jafaal-0.1.0/jafaal/webauthn/router.py +439 -0
- jafaal-0.1.0/jafaal/webauthn/schema.py +156 -0
- jafaal-0.1.0/jafaal/webauthn/service.py +420 -0
- jafaal-0.1.0/pyproject.toml +416 -0
jafaal-0.1.0/.gitignore
ADDED
|
@@ -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
|
jafaal-0.1.0/LICENSE.md
ADDED
|
@@ -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
|
+
[](https://github.com/endurain-project/jafaal/blob/main/LICENSE.md)
|
|
60
|
+
[](https://github.com/endurain-project/jafaal/releases)
|
|
61
|
+
[](https://pypi.org/project/jafaal/)
|
|
62
|
+
[](https://pypi.org/project/jafaal/)
|
|
63
|
+
[](https://pypi.org/project/jafaal/)
|
|
64
|
+
[](https://jafaal.endurain.com/)
|
|
65
|
+
[](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>
|