hawkapi-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.
@@ -0,0 +1,52 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ lint:
10
+ name: Lint
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v4
15
+ with:
16
+ enable-cache: true
17
+ - name: Install dependencies
18
+ run: uv sync --extra dev
19
+ - name: ruff check
20
+ run: uv run ruff check .
21
+ - name: ruff format check
22
+ run: uv run ruff format --check .
23
+
24
+ typecheck:
25
+ name: Typecheck
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: astral-sh/setup-uv@v4
30
+ with:
31
+ enable-cache: true
32
+ - name: Install dependencies
33
+ run: uv sync --extra dev
34
+ - name: pyright
35
+ run: uv run pyright src/
36
+
37
+ test:
38
+ name: Test (Python ${{ matrix.python-version }})
39
+ runs-on: ubuntu-latest
40
+ strategy:
41
+ matrix:
42
+ python-version: ["3.12", "3.13"]
43
+ steps:
44
+ - uses: actions/checkout@v4
45
+ - uses: astral-sh/setup-uv@v4
46
+ with:
47
+ enable-cache: true
48
+ python-version: ${{ matrix.python-version }}
49
+ - name: Install dependencies
50
+ run: uv sync --extra dev
51
+ - name: Run tests
52
+ run: uv run pytest tests/ -q
@@ -0,0 +1,25 @@
1
+ name: Release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build-and-publish:
9
+ name: Build and publish to PyPI
10
+ runs-on: ubuntu-latest
11
+ environment: release
12
+ permissions:
13
+ id-token: write # required for trusted publishing
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v4
18
+ with:
19
+ enable-cache: true
20
+ - name: Build package
21
+ run: uv build
22
+ - name: Publish to PyPI
23
+ uses: pypa/gh-action-pypi-publish@release/v1
24
+ with:
25
+ packages-dir: dist/
@@ -0,0 +1,35 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ *.egg
9
+ .eggs/
10
+ .venv/
11
+ venv/
12
+ env/
13
+ .env
14
+ *.log
15
+ .mypy_cache/
16
+ .pyright/
17
+ .ruff_cache/
18
+ .pytest_cache/
19
+ htmlcov/
20
+ .coverage
21
+ .coverage.*
22
+ coverage.xml
23
+ *.cover
24
+ .hypothesis/
25
+ .tox/
26
+ .nox/
27
+ *.swp
28
+ *.swo
29
+ *~
30
+ .DS_Store
31
+ .idea/
32
+ .vscode/
33
+ .history/
34
+ site/
35
+ .remember/
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-05-16
4
+
5
+ Initial release.
6
+
7
+ - JWT access + refresh tokens (HS256/384/512, RS*, ES*).
8
+ - argon2id password hashing with `needs_rehash`.
9
+ - DI guards: `requires_user`, `requires_claims`, `requires_scopes`.
10
+ - In-memory `RevocationList` with lazy expiry sweep.
11
+ - `init_auth(app, config=...)` plugin entry point.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HawkAPI Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.4
2
+ Name: hawkapi-auth
3
+ Version: 0.1.0
4
+ Summary: JWT auth for HawkAPI — access + refresh tokens, password hashing, DI guards
5
+ Project-URL: Homepage, https://pypi.org/project/hawkapi-auth/
6
+ Project-URL: Repository, https://github.com/ashimov/hawkapi-auth
7
+ Project-URL: Issues, https://github.com/ashimov/hawkapi-auth/issues
8
+ Author-email: HawkAPI Contributors <hawkapi@users.noreply.github.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 HawkAPI Contributors
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: argon2,auth,authentication,bcrypt,hawkapi,jwt
32
+ Classifier: Development Status :: 5 - Production/Stable
33
+ Classifier: Framework :: AsyncIO
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Programming Language :: Python :: 3.13
39
+ Classifier: Topic :: Internet :: WWW/HTTP
40
+ Classifier: Topic :: Security
41
+ Classifier: Typing :: Typed
42
+ Requires-Python: >=3.12
43
+ Requires-Dist: argon2-cffi>=23.1
44
+ Requires-Dist: hawkapi>=0.1.7
45
+ Requires-Dist: pyjwt>=2.8
46
+ Provides-Extra: dev
47
+ Requires-Dist: httpx>=0.27; extra == 'dev'
48
+ Requires-Dist: pyright>=1.1; extra == 'dev'
49
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
50
+ Requires-Dist: pytest>=8.0; extra == 'dev'
51
+ Requires-Dist: ruff>=0.8; extra == 'dev'
52
+ Description-Content-Type: text/markdown
53
+
54
+ # hawkapi-auth
55
+
56
+ JWT auth for [HawkAPI](https://github.com/ashimov/HawkAPI). Access + refresh tokens, argon2id password hashing, DI guards, scope-based access control.
57
+
58
+ ## Install
59
+
60
+ ```bash
61
+ pip install hawkapi-auth
62
+ ```
63
+
64
+ ## Quickstart
65
+
66
+ ```python
67
+ from hawkapi import Depends, HawkAPI, HTTPException
68
+ from hawkapi_auth import (
69
+ JWTConfig,
70
+ hash_password,
71
+ init_auth,
72
+ random_secret,
73
+ requires_user,
74
+ verify_password,
75
+ )
76
+
77
+ app = HawkAPI()
78
+ init_auth(app, config=JWTConfig(secret=random_secret()))
79
+
80
+
81
+ @app.post("/register")
82
+ async def register(email: str, password: str):
83
+ await db.create_user(email=email, password_hash=hash_password(password))
84
+ return {"ok": True}
85
+
86
+
87
+ @app.post("/login")
88
+ async def login(email: str, password: str):
89
+ user = await db.find_user(email)
90
+ if not user or not verify_password(password, user.password_hash):
91
+ raise HTTPException(401, detail="Invalid credentials")
92
+ issuer = app.state.auth
93
+ return {
94
+ "access_token": issuer.issue_access(user.id),
95
+ "refresh_token": issuer.issue_refresh(user.id),
96
+ }
97
+
98
+
99
+ @app.get("/me")
100
+ async def me(user_id: str = Depends(requires_user)):
101
+ return await db.fetch_user(user_id)
102
+ ```
103
+
104
+ ## Token issue / verify
105
+
106
+ ```python
107
+ issuer = app.state.auth # TokenIssuer
108
+
109
+ access = issuer.issue_access("user-1", role="admin", scope="read write")
110
+ refresh = issuer.issue_refresh("user-1")
111
+
112
+ claims = issuer.verify_access(access) # raises TokenError on bad token
113
+ claims = issuer.verify_refresh(refresh) # ditto, plus checks the token type
114
+ ```
115
+
116
+ `issue_access` / `issue_refresh` accept arbitrary keyword claims (`role`, `scope`, anything JSON-serialisable).
117
+
118
+ ## JWTConfig
119
+
120
+ ```python
121
+ JWTConfig(
122
+ secret="…", # HMAC secret for HS256/384/512
123
+ algorithm="HS256",
124
+ access_ttl_seconds=15 * 60,
125
+ refresh_ttl_seconds=30 * 24 * 60 * 60,
126
+ issuer="my-service", # optional iss claim
127
+ audience="my-api", # optional aud claim
128
+ private_key="", # RS*/ES* — PEM
129
+ public_key="",
130
+ )
131
+ ```
132
+
133
+ Use `random_secret()` to mint one. Store it outside of git.
134
+
135
+ ## DI guards
136
+
137
+ ```python
138
+ from hawkapi_auth import requires_user, requires_claims, requires_scopes
139
+
140
+ @app.get("/me")
141
+ async def me(user_id: str = Depends(requires_user)):
142
+ ...
143
+
144
+ @app.get("/dump")
145
+ async def dump(claims: dict = Depends(requires_claims)):
146
+ ...
147
+
148
+ @app.get("/admin", dependencies=[Depends(requires_scopes("admin"))])
149
+ async def admin():
150
+ ...
151
+ ```
152
+
153
+ `requires_scopes(*scopes)` expects either a space-separated `scope` claim or a list under `scope` / `scopes`. Missing scopes → 403.
154
+
155
+ ## Refresh + revocation
156
+
157
+ ```python
158
+ from hawkapi_auth import RevocationList
159
+
160
+ rev = RevocationList()
161
+ init_auth(app, config=JWTConfig(secret=...), revocation=rev)
162
+
163
+ @app.post("/refresh")
164
+ async def refresh(refresh_token: str):
165
+ issuer = app.state.auth
166
+ claims = issuer.verify_refresh(refresh_token)
167
+ return {"access_token": issuer.issue_access(claims["sub"])}
168
+
169
+ @app.post("/logout")
170
+ async def logout(refresh_token: str):
171
+ app.state.auth.revoke_refresh(refresh_token)
172
+ return {"ok": True}
173
+ ```
174
+
175
+ `RevocationList` is in-memory only. For multi-process deployments, swap in a Redis-backed implementation (planned in v0.2.0).
176
+
177
+ ## Password hashing
178
+
179
+ ```python
180
+ from hawkapi_auth import hash_password, verify_password, needs_rehash
181
+
182
+ h = hash_password("hunter2") # argon2id
183
+ ok = verify_password("hunter2", h) # constant-time, returns bool
184
+ if needs_rehash(h):
185
+ h = hash_password("hunter2") # re-hash after a successful login
186
+ ```
187
+
188
+ `verify_password` never raises — safe to use directly in handler bodies.
189
+
190
+ ## What's not included (v0.2.0 roadmap)
191
+
192
+ * Social OAuth providers (Google / GitHub / Discord / Microsoft).
193
+ * Email-based password reset + verification flows.
194
+ * Pre-built user model and storage.
195
+ * Redis-backed `RevocationList`.
196
+
197
+ ## Development
198
+
199
+ ```bash
200
+ git clone https://github.com/ashimov/hawkapi-auth.git
201
+ cd hawkapi-auth
202
+ uv sync --extra dev
203
+ uv run pytest -q
204
+ uv run ruff check . && uv run ruff format --check .
205
+ uv run pyright src/
206
+ ```
207
+
208
+ ## License
209
+
210
+ MIT.
@@ -0,0 +1,157 @@
1
+ # hawkapi-auth
2
+
3
+ JWT auth for [HawkAPI](https://github.com/ashimov/HawkAPI). Access + refresh tokens, argon2id password hashing, DI guards, scope-based access control.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install hawkapi-auth
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ ```python
14
+ from hawkapi import Depends, HawkAPI, HTTPException
15
+ from hawkapi_auth import (
16
+ JWTConfig,
17
+ hash_password,
18
+ init_auth,
19
+ random_secret,
20
+ requires_user,
21
+ verify_password,
22
+ )
23
+
24
+ app = HawkAPI()
25
+ init_auth(app, config=JWTConfig(secret=random_secret()))
26
+
27
+
28
+ @app.post("/register")
29
+ async def register(email: str, password: str):
30
+ await db.create_user(email=email, password_hash=hash_password(password))
31
+ return {"ok": True}
32
+
33
+
34
+ @app.post("/login")
35
+ async def login(email: str, password: str):
36
+ user = await db.find_user(email)
37
+ if not user or not verify_password(password, user.password_hash):
38
+ raise HTTPException(401, detail="Invalid credentials")
39
+ issuer = app.state.auth
40
+ return {
41
+ "access_token": issuer.issue_access(user.id),
42
+ "refresh_token": issuer.issue_refresh(user.id),
43
+ }
44
+
45
+
46
+ @app.get("/me")
47
+ async def me(user_id: str = Depends(requires_user)):
48
+ return await db.fetch_user(user_id)
49
+ ```
50
+
51
+ ## Token issue / verify
52
+
53
+ ```python
54
+ issuer = app.state.auth # TokenIssuer
55
+
56
+ access = issuer.issue_access("user-1", role="admin", scope="read write")
57
+ refresh = issuer.issue_refresh("user-1")
58
+
59
+ claims = issuer.verify_access(access) # raises TokenError on bad token
60
+ claims = issuer.verify_refresh(refresh) # ditto, plus checks the token type
61
+ ```
62
+
63
+ `issue_access` / `issue_refresh` accept arbitrary keyword claims (`role`, `scope`, anything JSON-serialisable).
64
+
65
+ ## JWTConfig
66
+
67
+ ```python
68
+ JWTConfig(
69
+ secret="…", # HMAC secret for HS256/384/512
70
+ algorithm="HS256",
71
+ access_ttl_seconds=15 * 60,
72
+ refresh_ttl_seconds=30 * 24 * 60 * 60,
73
+ issuer="my-service", # optional iss claim
74
+ audience="my-api", # optional aud claim
75
+ private_key="", # RS*/ES* — PEM
76
+ public_key="",
77
+ )
78
+ ```
79
+
80
+ Use `random_secret()` to mint one. Store it outside of git.
81
+
82
+ ## DI guards
83
+
84
+ ```python
85
+ from hawkapi_auth import requires_user, requires_claims, requires_scopes
86
+
87
+ @app.get("/me")
88
+ async def me(user_id: str = Depends(requires_user)):
89
+ ...
90
+
91
+ @app.get("/dump")
92
+ async def dump(claims: dict = Depends(requires_claims)):
93
+ ...
94
+
95
+ @app.get("/admin", dependencies=[Depends(requires_scopes("admin"))])
96
+ async def admin():
97
+ ...
98
+ ```
99
+
100
+ `requires_scopes(*scopes)` expects either a space-separated `scope` claim or a list under `scope` / `scopes`. Missing scopes → 403.
101
+
102
+ ## Refresh + revocation
103
+
104
+ ```python
105
+ from hawkapi_auth import RevocationList
106
+
107
+ rev = RevocationList()
108
+ init_auth(app, config=JWTConfig(secret=...), revocation=rev)
109
+
110
+ @app.post("/refresh")
111
+ async def refresh(refresh_token: str):
112
+ issuer = app.state.auth
113
+ claims = issuer.verify_refresh(refresh_token)
114
+ return {"access_token": issuer.issue_access(claims["sub"])}
115
+
116
+ @app.post("/logout")
117
+ async def logout(refresh_token: str):
118
+ app.state.auth.revoke_refresh(refresh_token)
119
+ return {"ok": True}
120
+ ```
121
+
122
+ `RevocationList` is in-memory only. For multi-process deployments, swap in a Redis-backed implementation (planned in v0.2.0).
123
+
124
+ ## Password hashing
125
+
126
+ ```python
127
+ from hawkapi_auth import hash_password, verify_password, needs_rehash
128
+
129
+ h = hash_password("hunter2") # argon2id
130
+ ok = verify_password("hunter2", h) # constant-time, returns bool
131
+ if needs_rehash(h):
132
+ h = hash_password("hunter2") # re-hash after a successful login
133
+ ```
134
+
135
+ `verify_password` never raises — safe to use directly in handler bodies.
136
+
137
+ ## What's not included (v0.2.0 roadmap)
138
+
139
+ * Social OAuth providers (Google / GitHub / Discord / Microsoft).
140
+ * Email-based password reset + verification flows.
141
+ * Pre-built user model and storage.
142
+ * Redis-backed `RevocationList`.
143
+
144
+ ## Development
145
+
146
+ ```bash
147
+ git clone https://github.com/ashimov/hawkapi-auth.git
148
+ cd hawkapi-auth
149
+ uv sync --extra dev
150
+ uv run pytest -q
151
+ uv run ruff check . && uv run ruff format --check .
152
+ uv run pyright src/
153
+ ```
154
+
155
+ ## License
156
+
157
+ MIT.
@@ -0,0 +1,72 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "hawkapi-auth"
7
+ version = "0.1.0"
8
+ description = "JWT auth for HawkAPI — access + refresh tokens, password hashing, DI guards"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.12"
12
+ authors = [
13
+ { name = "HawkAPI Contributors", email = "hawkapi@users.noreply.github.com" },
14
+ ]
15
+ keywords = ["hawkapi", "auth", "jwt", "authentication", "argon2", "bcrypt"]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Framework :: AsyncIO",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Internet :: WWW/HTTP",
25
+ "Topic :: Security",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = [
29
+ "hawkapi>=0.1.7",
30
+ "PyJWT>=2.8",
31
+ "argon2-cffi>=23.1",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://pypi.org/project/hawkapi-auth/"
36
+ Repository = "https://github.com/ashimov/hawkapi-auth"
37
+ Issues = "https://github.com/ashimov/hawkapi-auth/issues"
38
+
39
+ [project.optional-dependencies]
40
+ dev = [
41
+ "pytest>=8.0",
42
+ "pytest-asyncio>=0.24",
43
+ "httpx>=0.27",
44
+ "ruff>=0.8",
45
+ "pyright>=1.1",
46
+ ]
47
+
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["src/hawkapi_auth"]
50
+
51
+ [tool.pytest.ini_options]
52
+ testpaths = ["tests"]
53
+ asyncio_mode = "auto"
54
+ filterwarnings = ["ignore::DeprecationWarning"]
55
+
56
+ [tool.ruff]
57
+ target-version = "py312"
58
+ line-length = 100
59
+
60
+ [tool.ruff.lint]
61
+ select = ["E", "F", "I", "UP", "B", "SIM", "S"]
62
+ ignore = ["S101", "B008"]
63
+
64
+ [tool.ruff.lint.per-file-ignores]
65
+ "tests/**" = ["S"]
66
+
67
+ [tool.pyright]
68
+ pythonVersion = "3.12"
69
+ typeCheckingMode = "strict"
70
+ reportUnknownVariableType = false
71
+ reportUnknownMemberType = false
72
+ reportUnknownArgumentType = false
@@ -0,0 +1,32 @@
1
+ """hawkapi-auth — JWT auth (access + refresh) + password hashing for HawkAPI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ from hawkapi_auth._deps import requires_claims, requires_scopes, requires_user
8
+ from hawkapi_auth._passwords import hash_password, needs_rehash, verify_password
9
+ from hawkapi_auth._plugin import init_auth
10
+ from hawkapi_auth._tokens import (
11
+ JWTConfig,
12
+ RevocationList,
13
+ TokenError,
14
+ TokenIssuer,
15
+ random_secret,
16
+ )
17
+
18
+ __all__ = [
19
+ "JWTConfig",
20
+ "RevocationList",
21
+ "TokenError",
22
+ "TokenIssuer",
23
+ "__version__",
24
+ "hash_password",
25
+ "init_auth",
26
+ "needs_rehash",
27
+ "random_secret",
28
+ "requires_claims",
29
+ "requires_scopes",
30
+ "requires_user",
31
+ "verify_password",
32
+ ]