Skip to content

Commit be19a7a

Browse files
Berik AshimovBerik Ashimov
authored andcommitted
feat: initial release — JWT auth for HawkAPI v0.1.0
0 parents  commit be19a7a

17 files changed

Lines changed: 1433 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

.github/workflows/release.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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/

.gitignore

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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/

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.

README.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.

pyproject.toml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)