phzyx-auth 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 (39) hide show
  1. phzyx_auth-0.1.0/.gitignore +87 -0
  2. phzyx_auth-0.1.0/PKG-INFO +116 -0
  3. phzyx_auth-0.1.0/README.md +88 -0
  4. phzyx_auth-0.1.0/pyproject.toml +39 -0
  5. phzyx_auth-0.1.0/src/phzyx/auth/__init__.py +141 -0
  6. phzyx_auth-0.1.0/src/phzyx/auth/adapters/__init__.py +19 -0
  7. phzyx_auth-0.1.0/src/phzyx/auth/adapters/fastapi.py +638 -0
  8. phzyx_auth-0.1.0/src/phzyx/auth/adapters/litestar.py +280 -0
  9. phzyx_auth-0.1.0/src/phzyx/auth/admin_bridge.py +83 -0
  10. phzyx_auth-0.1.0/src/phzyx/auth/api_keys.py +94 -0
  11. phzyx_auth-0.1.0/src/phzyx/auth/audit.py +36 -0
  12. phzyx_auth-0.1.0/src/phzyx/auth/cli.py +43 -0
  13. phzyx_auth-0.1.0/src/phzyx/auth/config.py +92 -0
  14. phzyx_auth-0.1.0/src/phzyx/auth/crypto.py +19 -0
  15. phzyx_auth-0.1.0/src/phzyx/auth/csrf.py +45 -0
  16. phzyx_auth-0.1.0/src/phzyx/auth/decorators.py +116 -0
  17. phzyx_auth-0.1.0/src/phzyx/auth/errors.py +64 -0
  18. phzyx_auth-0.1.0/src/phzyx/auth/guards.py +67 -0
  19. phzyx_auth-0.1.0/src/phzyx/auth/models.py +238 -0
  20. phzyx_auth-0.1.0/src/phzyx/auth/oauth.py +249 -0
  21. phzyx_auth-0.1.0/src/phzyx/auth/orm_stores.py +436 -0
  22. phzyx_auth-0.1.0/src/phzyx/auth/passkeys.py +351 -0
  23. phzyx_auth-0.1.0/src/phzyx/auth/password.py +56 -0
  24. phzyx_auth-0.1.0/src/phzyx/auth/principal.py +73 -0
  25. phzyx_auth-0.1.0/src/phzyx/auth/rate_limit.py +76 -0
  26. phzyx_auth-0.1.0/src/phzyx/auth/rbac.py +75 -0
  27. phzyx_auth-0.1.0/src/phzyx/auth/service.py +956 -0
  28. phzyx_auth-0.1.0/src/phzyx/auth/service_async.py +77 -0
  29. phzyx_auth-0.1.0/src/phzyx/auth/sessions.py +117 -0
  30. phzyx_auth-0.1.0/src/phzyx/auth/tokens.py +158 -0
  31. phzyx_auth-0.1.0/src/phzyx/auth/totp.py +105 -0
  32. phzyx_auth-0.1.0/tests/test_auth_fastapi_mount.py +184 -0
  33. phzyx_auth-0.1.0/tests/test_auth_foundation.py +165 -0
  34. phzyx_auth-0.1.0/tests/test_auth_litestar.py +84 -0
  35. phzyx_auth-0.1.0/tests/test_auth_orm_stores.py +97 -0
  36. phzyx_auth-0.1.0/tests/test_auth_service_flows.py +193 -0
  37. phzyx_auth-0.1.0/tests/test_oauth_mfa.py +150 -0
  38. phzyx_auth-0.1.0/tests/test_passkeys.py +97 -0
  39. phzyx_auth-0.1.0/tests/test_rate_limit_redis.py +70 -0
@@ -0,0 +1,87 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ *.egg-info/
8
+ .eggs/
9
+ dist/
10
+ !packages/admin/web/dist/
11
+ !packages/admin/web/dist/**
12
+ build/
13
+ *.egg
14
+
15
+ # Virtual environments
16
+ .venv/
17
+ venv/
18
+ ENV/
19
+ examples/**/.venv/
20
+
21
+ # uv / packaging
22
+ # uv.lock is committed for reproducible workspace installs
23
+
24
+ # Testing / coverage
25
+ .pytest_cache/
26
+ .hypothesis/
27
+ .coverage
28
+ .coverage.*
29
+ coverage.xml
30
+ coverage.json
31
+ htmlcov/
32
+ .mypy_cache/
33
+ .ruff_cache/
34
+ .dmypy.json
35
+ dmypy.json
36
+ .pytype/
37
+ .pyre/
38
+
39
+ # Type checkers / IDE
40
+ .idea/
41
+ .vscode/
42
+ *.swp
43
+ *.swo
44
+ *~
45
+
46
+ # OS
47
+ .DS_Store
48
+ Thumbs.db
49
+
50
+ # Env / secrets
51
+ .env
52
+ .env.*
53
+ !.env.example
54
+
55
+ # Node (AdminBoard SPA + sites)
56
+ node_modules/
57
+ .pnpm-store/
58
+ packages/admin/web/node_modules/
59
+ # Built SPA is committed for offline wheel/demo convenience (M4);
60
+ # rebuild via packages/admin/web `npm run build` when sources change.
61
+ # packages/admin/web/dist/
62
+
63
+ # Local tooling
64
+ .justfile.local
65
+ packages/admin/web/node_modules/
66
+ dist-release/
67
+
68
+ # Local demo SQLite
69
+ examples/**/*.db
70
+ examples/**/media/
71
+
72
+
73
+ # Admin web tooling
74
+ packages/admin/web/test-results/
75
+ packages/admin/web/playwright-report/
76
+ packages/admin/web/blob-report/
77
+
78
+ # Private: research, design plans, AI agent helpers (local only — do not push)
79
+ # Files remain on disk; they are untracked for remotes.
80
+ research/
81
+ AGENTS.md
82
+ AGENT_TO_AGENT.md
83
+ docs/agent-rules.md
84
+ .cursor/
85
+ .claude/
86
+ .grok/
87
+
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: phzyx-auth
3
+ Version: 0.1.0
4
+ Summary: Phzyx Auth — secure-by-default identity, sessions, JWT, API keys, RBAC (Forge battery)
5
+ Project-URL: Homepage, https://phzyx.xyz
6
+ Project-URL: Repository, https://github.com/phzyxyz/forge
7
+ Author: Athul Nandaswaroop
8
+ License-Expression: MIT
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: argon2-cffi>=23
11
+ Requires-Dist: phzyx-forge<0.2,>=0.1.0
12
+ Requires-Dist: pydantic>=2
13
+ Requires-Dist: pyjwt>=2.8
14
+ Provides-Extra: fastapi
15
+ Requires-Dist: fastapi>=0.115; extra == 'fastapi'
16
+ Provides-Extra: litestar
17
+ Requires-Dist: litestar>=2.12; extra == 'litestar'
18
+ Provides-Extra: mfa
19
+ Requires-Dist: pyotp>=2.9; extra == 'mfa'
20
+ Provides-Extra: oauth
21
+ Requires-Dist: authlib>=1.3; extra == 'oauth'
22
+ Requires-Dist: httpx>=0.27; extra == 'oauth'
23
+ Provides-Extra: redis
24
+ Requires-Dist: redis>=5; extra == 'redis'
25
+ Provides-Extra: webauthn
26
+ Requires-Dist: webauthn>=2.0; extra == 'webauthn'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # phzyx-auth
30
+
31
+ Secure-by-default authentication & authorization for Phzyx apps and AdminBoard.
32
+
33
+ **Public battery** for Forge (depends on `phzyx-forge` on PyPI).
34
+
35
+ ```bash
36
+ uv add 'phzyx-forge[auth]'
37
+ # or
38
+ uv add 'phzyx-auth[fastapi]'
39
+ ```
40
+
41
+ ## Status (0.1.0)
42
+
43
+ **Phase A + B shipped** (memory backends; ORM-backed stores next):
44
+
45
+ | Area | What you get |
46
+ |------|----------------|
47
+ | Passwords | Argon2id, policy min length |
48
+ | Sessions | Opaque cookie sessions (hashed at rest), idle + absolute TTL |
49
+ | JWT | HS256 access + rotating refresh |
50
+ | API keys | `phx_` prefix, scopes, list/revoke |
51
+ | RBAC | Groups/Permissions models, Principal, `ensure_*` + Depends + decorators |
52
+ | Lockout | Failed-login counter + rate limits + audit log |
53
+ | Reset / change | Forgot/reset (anti-enum), change password, revoke sessions |
54
+ | Email verify | Token issue + verify (`email_verified` flag) |
55
+ | CSRF | Double-submit cookie + `X-CSRF-Token` for session mutations |
56
+ | WebSocket | `websocket_principal` (cookie / Bearer / API key, close 4401) |
57
+ | AdminBoard | `PhzyxAuthz` permission bridge |
58
+ | CLI | `phzyx-auth create-superuser` |
59
+
60
+ **Later:** OAuth IdPs (Phase D stub in `oauth.py`), MFA/passkeys (E), orgs (F), Redis rate limit, ORM session store.
61
+
62
+ ## Quick start (FastAPI)
63
+
64
+ ```python
65
+ from fastapi import Depends, FastAPI
66
+ from phzyx.auth import AuthConfig, AuthService, Principal
67
+ from phzyx.auth.adapters.fastapi import mount_auth, require_user
68
+
69
+ app = FastAPI()
70
+ svc = AuthService(
71
+ config=AuthConfig(
72
+ secret="use-PHZYX_AUTH_SECRET-32+bytes-in-prod!!",
73
+ production=False,
74
+ cookie_secure=False,
75
+ )
76
+ )
77
+ mount_auth(app, svc)
78
+
79
+ @app.get("/private")
80
+ async def private(user: Principal = Depends(require_user)):
81
+ return {"email": user.email}
82
+ ```
83
+
84
+ ```bash
85
+ # create staff superuser (in-memory unless you wire persistence)
86
+ phzyx-auth create-superuser --email admin@example.com --password 'longpassword1'
87
+ ```
88
+
89
+ ### SPA cookie flow
90
+
91
+ 1. `POST /auth/login` → sets `phzyx_session` (HttpOnly) + `phzyx_csrf` + returns `csrf_token`
92
+ 2. Mutating requests with the session cookie must send header `X-CSRF-Token: <csrf_token>`
93
+ 3. Bearer JWT / `X-API-Key` skip CSRF
94
+
95
+ ### In-body checks (no middleware)
96
+
97
+ ```python
98
+ from phzyx.auth.guards import ensure_authenticated, ensure_permissions
99
+
100
+ p = svc.resolve_principal(session_token=cookie, authorization=authz, api_key=key)
101
+ user = ensure_authenticated(p)
102
+ ensure_permissions(user, ["posts:create"])
103
+ ```
104
+
105
+ ## Security defaults
106
+
107
+ - Argon2id hashing
108
+ - Secrets ≥ 32 bytes (HS256 / pepper)
109
+ - Opaque sessions + short JWT access
110
+ - API keys hashed at rest
111
+ - Lockout + IP/email rate limits
112
+ - Password reset anti-enumeration
113
+ - Extensible `AbstractUser`
114
+
115
+ **Research (do not re-derive):** [`research/plan/20_phzyx_auth.md`](../../research/plan/20_phzyx_auth.md)
116
+ **Tracker:** [`PHZYX_ROADMAP.html`](../../PHZYX_ROADMAP.html) track **AUTH**.
@@ -0,0 +1,88 @@
1
+ # phzyx-auth
2
+
3
+ Secure-by-default authentication & authorization for Phzyx apps and AdminBoard.
4
+
5
+ **Public battery** for Forge (depends on `phzyx-forge` on PyPI).
6
+
7
+ ```bash
8
+ uv add 'phzyx-forge[auth]'
9
+ # or
10
+ uv add 'phzyx-auth[fastapi]'
11
+ ```
12
+
13
+ ## Status (0.1.0)
14
+
15
+ **Phase A + B shipped** (memory backends; ORM-backed stores next):
16
+
17
+ | Area | What you get |
18
+ |------|----------------|
19
+ | Passwords | Argon2id, policy min length |
20
+ | Sessions | Opaque cookie sessions (hashed at rest), idle + absolute TTL |
21
+ | JWT | HS256 access + rotating refresh |
22
+ | API keys | `phx_` prefix, scopes, list/revoke |
23
+ | RBAC | Groups/Permissions models, Principal, `ensure_*` + Depends + decorators |
24
+ | Lockout | Failed-login counter + rate limits + audit log |
25
+ | Reset / change | Forgot/reset (anti-enum), change password, revoke sessions |
26
+ | Email verify | Token issue + verify (`email_verified` flag) |
27
+ | CSRF | Double-submit cookie + `X-CSRF-Token` for session mutations |
28
+ | WebSocket | `websocket_principal` (cookie / Bearer / API key, close 4401) |
29
+ | AdminBoard | `PhzyxAuthz` permission bridge |
30
+ | CLI | `phzyx-auth create-superuser` |
31
+
32
+ **Later:** OAuth IdPs (Phase D stub in `oauth.py`), MFA/passkeys (E), orgs (F), Redis rate limit, ORM session store.
33
+
34
+ ## Quick start (FastAPI)
35
+
36
+ ```python
37
+ from fastapi import Depends, FastAPI
38
+ from phzyx.auth import AuthConfig, AuthService, Principal
39
+ from phzyx.auth.adapters.fastapi import mount_auth, require_user
40
+
41
+ app = FastAPI()
42
+ svc = AuthService(
43
+ config=AuthConfig(
44
+ secret="use-PHZYX_AUTH_SECRET-32+bytes-in-prod!!",
45
+ production=False,
46
+ cookie_secure=False,
47
+ )
48
+ )
49
+ mount_auth(app, svc)
50
+
51
+ @app.get("/private")
52
+ async def private(user: Principal = Depends(require_user)):
53
+ return {"email": user.email}
54
+ ```
55
+
56
+ ```bash
57
+ # create staff superuser (in-memory unless you wire persistence)
58
+ phzyx-auth create-superuser --email admin@example.com --password 'longpassword1'
59
+ ```
60
+
61
+ ### SPA cookie flow
62
+
63
+ 1. `POST /auth/login` → sets `phzyx_session` (HttpOnly) + `phzyx_csrf` + returns `csrf_token`
64
+ 2. Mutating requests with the session cookie must send header `X-CSRF-Token: <csrf_token>`
65
+ 3. Bearer JWT / `X-API-Key` skip CSRF
66
+
67
+ ### In-body checks (no middleware)
68
+
69
+ ```python
70
+ from phzyx.auth.guards import ensure_authenticated, ensure_permissions
71
+
72
+ p = svc.resolve_principal(session_token=cookie, authorization=authz, api_key=key)
73
+ user = ensure_authenticated(p)
74
+ ensure_permissions(user, ["posts:create"])
75
+ ```
76
+
77
+ ## Security defaults
78
+
79
+ - Argon2id hashing
80
+ - Secrets ≥ 32 bytes (HS256 / pepper)
81
+ - Opaque sessions + short JWT access
82
+ - API keys hashed at rest
83
+ - Lockout + IP/email rate limits
84
+ - Password reset anti-enumeration
85
+ - Extensible `AbstractUser`
86
+
87
+ **Research (do not re-derive):** [`research/plan/20_phzyx_auth.md`](../../research/plan/20_phzyx_auth.md)
88
+ **Tracker:** [`PHZYX_ROADMAP.html`](../../PHZYX_ROADMAP.html) track **AUTH**.
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "phzyx-auth"
3
+ version = "0.1.0"
4
+ description = "Phzyx Auth — secure-by-default identity, sessions, JWT, API keys, RBAC (Forge battery)"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "MIT"
8
+ authors = [{ name = "Athul Nandaswaroop" }]
9
+ dependencies = [
10
+ "phzyx-forge>=0.1.0,<0.2",
11
+ "pydantic>=2",
12
+ "argon2-cffi>=23",
13
+ "PyJWT>=2.8",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ redis = ["redis>=5"]
18
+ oauth = ["authlib>=1.3", "httpx>=0.27"]
19
+ mfa = ["pyotp>=2.9"]
20
+ webauthn = ["webauthn>=2.0"]
21
+ fastapi = ["fastapi>=0.115"]
22
+ litestar = ["litestar>=2.12"]
23
+
24
+ [project.scripts]
25
+ phzyx-auth = "phzyx.auth.cli:main"
26
+
27
+ [project.urls]
28
+ Homepage = "https://phzyx.xyz"
29
+ Repository = "https://github.com/phzyxyz/forge"
30
+
31
+ [tool.uv.sources]
32
+ phzyx-forge = { workspace = true }
33
+
34
+ [build-system]
35
+ requires = ["hatchling"]
36
+ build-backend = "hatchling.build"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["src/phzyx"]
@@ -0,0 +1,141 @@
1
+ """Phzyx Auth — identity, sessions, JWT, API keys, guards (batteries optional)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from phzyx.auth.admin_bridge import PhzyxAuthz, PhzyxFieldAuthz
8
+ from phzyx.auth.api_keys import IssuedAPIKey, MemoryAPIKeyStore
9
+ from phzyx.auth.audit import AuditLog
10
+ from phzyx.auth.config import AuthConfig
11
+ from phzyx.auth.decorators import (
12
+ require_auth,
13
+ require_permissions,
14
+ require_scopes,
15
+ require_staff,
16
+ require_superuser,
17
+ )
18
+ from phzyx.auth.errors import (
19
+ AuthError,
20
+ ForbiddenError,
21
+ InvalidCredentialsError,
22
+ LockedError,
23
+ MfaRequiredError,
24
+ UnauthorizedError,
25
+ ValidationAuthError,
26
+ )
27
+ from phzyx.auth.models import (
28
+ AUTH_MODELS,
29
+ AbstractUser,
30
+ Group,
31
+ MFAFactor,
32
+ OAuthAccount,
33
+ Permission,
34
+ User,
35
+ )
36
+ from phzyx.auth.oauth import (
37
+ AuthlibOAuthProvider,
38
+ OAuthNotConfiguredError,
39
+ OAuthProviderConfig,
40
+ StaticOAuthProvider,
41
+ build_provider,
42
+ preset_config,
43
+ )
44
+ from phzyx.auth.orm_stores import (
45
+ HybridUserMap,
46
+ OrmAPIKeyStore,
47
+ OrmPasswordResetStore,
48
+ OrmSessionStore,
49
+ OrmUserRepository,
50
+ )
51
+ from phzyx.auth.passkeys import HmacPasskeyBackend, default_passkey_backend, sign_hmac_assertion
52
+ from phzyx.auth.password import PasswordHasher, generate_token, hash_password, verify_password
53
+ from phzyx.auth.principal import Principal, principal_from_user
54
+ from phzyx.auth.rate_limit import MemoryRateLimiter, RedisRateLimiter
55
+ from phzyx.auth.rbac import has_permission, resolve_permissions, seed_resource_permissions
56
+ from phzyx.auth.service import AuthResult, AuthService, MfaChallenge
57
+ from phzyx.auth.service_async import AsyncAuthService
58
+ from phzyx.auth.sessions import MemorySessionStore
59
+ from phzyx.auth.tokens import JwtService, TokenPair
60
+
61
+ __version__ = "0.1.0"
62
+
63
+ __all__ = [
64
+ "AUTH_MODELS",
65
+ "AbstractUser",
66
+ "AsyncAuthService",
67
+ "AuditLog",
68
+ "AuthConfig",
69
+ "AuthError",
70
+ "AuthResult",
71
+ "AuthService",
72
+ "AuthlibOAuthProvider",
73
+ "ForbiddenError",
74
+ "Group",
75
+ "HmacPasskeyBackend",
76
+ "InvalidCredentialsError",
77
+ "IssuedAPIKey",
78
+ "JwtService",
79
+ "LockedError",
80
+ "HybridUserMap",
81
+ "MFAFactor",
82
+ "MemoryAPIKeyStore",
83
+ "MemoryRateLimiter",
84
+ "MemorySessionStore",
85
+ "MfaChallenge",
86
+ "MfaRequiredError",
87
+ "OAuthAccount",
88
+ "OAuthNotConfiguredError",
89
+ "OAuthProviderConfig",
90
+ "OrmAPIKeyStore",
91
+ "OrmPasswordResetStore",
92
+ "OrmSessionStore",
93
+ "OrmUserRepository",
94
+ "PasswordHasher",
95
+ "Permission",
96
+ "PhzyxAuthz",
97
+ "PhzyxFieldAuthz",
98
+ "Principal",
99
+ "RedisRateLimiter",
100
+ "StaticOAuthProvider",
101
+ "TokenPair",
102
+ "UnauthorizedError",
103
+ "User",
104
+ "ValidationAuthError",
105
+ "__version__",
106
+ "build_provider",
107
+ "create_superuser",
108
+ "default_passkey_backend",
109
+ "generate_token",
110
+ "has_permission",
111
+ "hash_password",
112
+ "mount_auth",
113
+ "mount_auth_litestar",
114
+ "preset_config",
115
+ "principal_from_user",
116
+ "require_auth",
117
+ "require_permissions",
118
+ "require_scopes",
119
+ "require_staff",
120
+ "require_superuser",
121
+ "resolve_permissions",
122
+ "sign_hmac_assertion",
123
+ "seed_resource_permissions",
124
+ "verify_password",
125
+ ]
126
+
127
+
128
+ def __getattr__(name: str) -> Any:
129
+ if name == "mount_auth":
130
+ from phzyx.auth.adapters.fastapi import mount_auth
131
+
132
+ return mount_auth
133
+ if name == "mount_auth_litestar":
134
+ from phzyx.auth.adapters.litestar import mount_auth_litestar
135
+
136
+ return mount_auth_litestar
137
+ if name == "create_superuser":
138
+ from phzyx.auth.cli import create_superuser
139
+
140
+ return create_superuser
141
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,19 @@
1
+ """Framework adapters (FastAPI, Litestar, …)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ __all__ = ["mount_auth", "mount_auth_litestar"]
8
+
9
+
10
+ def __getattr__(name: str) -> Any:
11
+ if name == "mount_auth":
12
+ from phzyx.auth.adapters.fastapi import mount_auth
13
+
14
+ return mount_auth
15
+ if name == "mount_auth_litestar":
16
+ from phzyx.auth.adapters.litestar import mount_auth_litestar
17
+
18
+ return mount_auth_litestar
19
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")