fauth 0.2.0__tar.gz → 0.2.2__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.
- fauth-0.2.2/.github/actions/setup-python-env/action.yml +34 -0
- fauth-0.2.2/.github/workflows/cicd.yml +136 -0
- {fauth-0.2.0 → fauth-0.2.2}/.github/workflows/pre-commit-autoupdate.yml +8 -16
- {fauth-0.2.0 → fauth-0.2.2}/.pre-commit-config.yaml +1 -1
- {fauth-0.2.0 → fauth-0.2.2}/CHANGELOG.md +66 -0
- {fauth-0.2.0 → fauth-0.2.2}/PKG-INFO +172 -37
- {fauth-0.2.0 → fauth-0.2.2}/README.md +171 -36
- {fauth-0.2.0 → fauth-0.2.2}/fauth/providers/protocols.py +6 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/providers/provider.py +43 -3
- {fauth-0.2.0 → fauth-0.2.2}/fauth/testing/__init__.py +2 -1
- {fauth-0.2.0 → fauth-0.2.2}/fauth/testing/fakes.py +11 -0
- {fauth-0.2.0 → fauth-0.2.2}/pyproject.toml +1 -1
- {fauth-0.2.0 → fauth-0.2.2}/tests/providers/conftest.py +35 -4
- {fauth-0.2.0 → fauth-0.2.2}/tests/providers/test_provider.py +83 -0
- {fauth-0.2.0 → fauth-0.2.2}/uv.lock +46 -46
- fauth-0.2.0/.github/workflows/cicd.yml +0 -181
- fauth-0.2.0/requirements.txt +0 -54
- {fauth-0.2.0 → fauth-0.2.2}/.github/dependabot.yml +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/.gitignore +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/LICENSE +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/api/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/api/router.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/core/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/core/config.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/core/exceptions.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/core/schemas.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/crypto/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/crypto/jwt.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/crypto/password.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/providers/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/testing/config.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/testing/provider.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/transports/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/transports/base.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/transports/bearer.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/utils/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/fauth/utils/logging.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/pytest.ini +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/api/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/api/conftest.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/api/test_openapi.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/api/test_router.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/conftest.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/core/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/core/conftest.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/core/test_config.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/core/test_exceptions.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/crypto/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/crypto/conftest.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/crypto/test_jwt.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/crypto/test_password.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/providers/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/testing/__init__.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/testing/test_testing.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/utils/conftest.py +0 -0
- {fauth-0.2.0 → fauth-0.2.2}/tests/utils/test_logging.py +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: 'Setup Python Environment'
|
|
2
|
+
description: 'Install Python, uv, and project dependencies'
|
|
3
|
+
|
|
4
|
+
inputs:
|
|
5
|
+
python-version:
|
|
6
|
+
description: 'Python version to set up'
|
|
7
|
+
required: true
|
|
8
|
+
uv-version:
|
|
9
|
+
description: 'uv version to install'
|
|
10
|
+
required: false
|
|
11
|
+
default: '0.9.11'
|
|
12
|
+
install-dependencies:
|
|
13
|
+
description: 'Whether to run uv sync after setup'
|
|
14
|
+
required: false
|
|
15
|
+
default: 'true'
|
|
16
|
+
|
|
17
|
+
runs:
|
|
18
|
+
using: 'composite'
|
|
19
|
+
steps:
|
|
20
|
+
- name: Set up Python ${{ inputs.python-version }}
|
|
21
|
+
uses: actions/setup-python@v6
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ inputs.python-version }}
|
|
24
|
+
|
|
25
|
+
- name: Install uv
|
|
26
|
+
uses: astral-sh/setup-uv@v7
|
|
27
|
+
with:
|
|
28
|
+
version: ${{ inputs.uv-version }}
|
|
29
|
+
python-version: ${{ inputs.python-version }}
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
if: inputs.install-dependencies == 'true'
|
|
33
|
+
shell: bash
|
|
34
|
+
run: uv sync --all-groups --frozen
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: ["main"]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
id-token: write
|
|
11
|
+
contents: write
|
|
12
|
+
pull-requests: write
|
|
13
|
+
actions: write
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
lint:
|
|
17
|
+
name: Lint (Python ${{ matrix.python-version }})
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
strategy:
|
|
20
|
+
matrix:
|
|
21
|
+
python-version: ["3.12", "3.13", "3.14"]
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v6
|
|
24
|
+
with:
|
|
25
|
+
token: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
|
|
26
|
+
fetch-depth: 0
|
|
27
|
+
|
|
28
|
+
- name: Setup environment
|
|
29
|
+
uses: ./.github/actions/setup-python-env
|
|
30
|
+
with:
|
|
31
|
+
python-version: ${{ matrix.python-version }}
|
|
32
|
+
|
|
33
|
+
- name: Run lint and format checks
|
|
34
|
+
run: uv run poe format
|
|
35
|
+
|
|
36
|
+
test:
|
|
37
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
strategy:
|
|
40
|
+
matrix:
|
|
41
|
+
python-version: ["3.12", "3.13", "3.14"]
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v6
|
|
44
|
+
with:
|
|
45
|
+
token: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
|
|
46
|
+
fetch-depth: 0
|
|
47
|
+
|
|
48
|
+
- name: Setup environment
|
|
49
|
+
uses: ./.github/actions/setup-python-env
|
|
50
|
+
with:
|
|
51
|
+
python-version: ${{ matrix.python-version }}
|
|
52
|
+
|
|
53
|
+
- name: Run unit tests
|
|
54
|
+
env:
|
|
55
|
+
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
|
56
|
+
run: uv run poe test
|
|
57
|
+
|
|
58
|
+
update-hooks:
|
|
59
|
+
name: Update pre-commit hooks
|
|
60
|
+
if: github.event_name == 'pull_request'
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v6
|
|
64
|
+
with:
|
|
65
|
+
token: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
|
|
66
|
+
ref: ${{ github.head_ref }}
|
|
67
|
+
fetch-depth: 0
|
|
68
|
+
|
|
69
|
+
- name: Setup environment
|
|
70
|
+
uses: ./.github/actions/setup-python-env
|
|
71
|
+
with:
|
|
72
|
+
python-version: "3.13"
|
|
73
|
+
|
|
74
|
+
- name: Autoupdate pre-commit hooks
|
|
75
|
+
run: uv run pre-commit autoupdate
|
|
76
|
+
|
|
77
|
+
- name: Commit updated hooks
|
|
78
|
+
run: |
|
|
79
|
+
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
|
80
|
+
git config --local user.name "github-actions[bot]"
|
|
81
|
+
git add .pre-commit-config.yaml
|
|
82
|
+
git diff --staged --quiet || git commit -m "chore(config): update pre-commit hooks"
|
|
83
|
+
git push origin HEAD:${{ github.head_ref }}
|
|
84
|
+
|
|
85
|
+
versioning:
|
|
86
|
+
name: Versioning
|
|
87
|
+
runs-on: ubuntu-latest
|
|
88
|
+
if: github.ref == 'refs/heads/main'
|
|
89
|
+
needs: [lint, test]
|
|
90
|
+
steps:
|
|
91
|
+
- uses: actions/checkout@v6
|
|
92
|
+
with:
|
|
93
|
+
token: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
|
|
94
|
+
ref: main
|
|
95
|
+
fetch-depth: 0
|
|
96
|
+
|
|
97
|
+
- name: Setup environment
|
|
98
|
+
uses: ./.github/actions/setup-python-env
|
|
99
|
+
with:
|
|
100
|
+
python-version: "3.13"
|
|
101
|
+
|
|
102
|
+
- name: Bump version and update changelog
|
|
103
|
+
id: release
|
|
104
|
+
uses: python-semantic-release/python-semantic-release@v9.21.1
|
|
105
|
+
with:
|
|
106
|
+
github_token: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
|
|
107
|
+
|
|
108
|
+
outputs:
|
|
109
|
+
released: ${{ steps.release.outputs.released }}
|
|
110
|
+
|
|
111
|
+
publish:
|
|
112
|
+
name: Publish to PyPI
|
|
113
|
+
runs-on: ubuntu-latest
|
|
114
|
+
if: needs.versioning.outputs.released == 'true'
|
|
115
|
+
needs: [versioning]
|
|
116
|
+
environment:
|
|
117
|
+
name: pypi
|
|
118
|
+
url: https://pypi.org/p/fauth
|
|
119
|
+
steps:
|
|
120
|
+
- uses: actions/checkout@v6
|
|
121
|
+
with:
|
|
122
|
+
token: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }}
|
|
123
|
+
ref: main
|
|
124
|
+
fetch-depth: 0
|
|
125
|
+
|
|
126
|
+
- name: Setup environment
|
|
127
|
+
uses: ./.github/actions/setup-python-env
|
|
128
|
+
with:
|
|
129
|
+
python-version: "3.13"
|
|
130
|
+
install-dependencies: "false"
|
|
131
|
+
|
|
132
|
+
- name: Build package
|
|
133
|
+
run: uv build
|
|
134
|
+
|
|
135
|
+
- name: Publish to PyPI
|
|
136
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -5,8 +5,8 @@ name: Pre-commit Update
|
|
|
5
5
|
|
|
6
6
|
on:
|
|
7
7
|
schedule:
|
|
8
|
-
- cron: '0 2 * *
|
|
9
|
-
workflow_dispatch:
|
|
8
|
+
- cron: '0 2 * * 1' # Mondays at 2 AM UTC
|
|
9
|
+
workflow_dispatch:
|
|
10
10
|
|
|
11
11
|
permissions:
|
|
12
12
|
contents: read
|
|
@@ -20,34 +20,26 @@ jobs:
|
|
|
20
20
|
runs-on: ubuntu-latest
|
|
21
21
|
steps:
|
|
22
22
|
- uses: actions/checkout@v6
|
|
23
|
-
- uses: actions/setup-python@v6
|
|
24
23
|
with:
|
|
25
|
-
|
|
24
|
+
token: ${{ secrets.GH_TOKEN }}
|
|
25
|
+
fetch-depth: 0
|
|
26
26
|
|
|
27
|
-
- name:
|
|
28
|
-
uses:
|
|
27
|
+
- name: Setup environment
|
|
28
|
+
uses: ./.github/actions/setup-python-env
|
|
29
29
|
with:
|
|
30
|
-
version: '0.9.11'
|
|
31
30
|
python-version: '3.13'
|
|
32
31
|
|
|
33
|
-
- name: Install dependencies
|
|
34
|
-
run: |
|
|
35
|
-
uv sync --all-groups --frozen
|
|
36
|
-
|
|
37
32
|
- name: Autoupdate pre-commit hooks
|
|
38
|
-
working-directory: '.'
|
|
39
33
|
run: uv run pre-commit autoupdate
|
|
40
34
|
|
|
41
35
|
- name: Create Pull Request
|
|
42
36
|
uses: peter-evans/create-pull-request@v8
|
|
43
|
-
env:
|
|
44
|
-
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
45
37
|
with:
|
|
46
|
-
token: ${{
|
|
38
|
+
token: ${{ secrets.GH_TOKEN }}
|
|
47
39
|
branch: chore/pre-commit-autoupdate
|
|
48
40
|
title: Update pre-commit hooks
|
|
49
41
|
commit-message: 'chore(config): update pre-commit hooks'
|
|
50
42
|
body: |
|
|
51
43
|
This PR was created automatically by the pre-commit autoupdate workflow.
|
|
52
44
|
It updates the pre-commit hooks to their latest versions.
|
|
53
|
-
labels: update
|
|
45
|
+
labels: update-hooks
|
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.2.2 (2026-04-04)
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- Add badge for dependabot
|
|
9
|
+
([`22f4719`](https://github.com/justmatias/fauth/commit/22f47195a489fbd9a63f5772c1081b887a2b0ea1))
|
|
10
|
+
|
|
11
|
+
### Chores
|
|
12
|
+
|
|
13
|
+
- Add python version matrix to CI/CD workflow
|
|
14
|
+
([`9576512`](https://github.com/justmatias/fauth/commit/9576512733ed709c9bce62a97101d397a010ace8))
|
|
15
|
+
|
|
16
|
+
- Fix cicd for dependabot prs
|
|
17
|
+
([`befb729`](https://github.com/justmatias/fauth/commit/befb729fca3e37ba23c5158b29659f712455f6ac))
|
|
18
|
+
|
|
19
|
+
- Implement setup env action and remove legacy requirements.txt
|
|
20
|
+
([`781a552`](https://github.com/justmatias/fauth/commit/781a552e29c3d658247762c5cf13bb66042976fb))
|
|
21
|
+
|
|
22
|
+
- Remove extract curly brace
|
|
23
|
+
([`2661dd3`](https://github.com/justmatias/fauth/commit/2661dd3f94da79909fa900d6d6dc341e199f5bb4))
|
|
24
|
+
|
|
25
|
+
- Restore checkout step in jobs
|
|
26
|
+
([`ad4e33d`](https://github.com/justmatias/fauth/commit/ad4e33d1b2575f368b7cb256c1bf9d34b52855dd))
|
|
27
|
+
|
|
28
|
+
- Set python version to min supported
|
|
29
|
+
([`8c981a4`](https://github.com/justmatias/fauth/commit/8c981a40939df8230ba1f776cf1dc6f57090ebad))
|
|
30
|
+
|
|
31
|
+
- **config**: Update pre-commit hooks
|
|
32
|
+
([`ab09f61`](https://github.com/justmatias/fauth/commit/ab09f6185058cece84ddf5ef371b268b7fb75429))
|
|
33
|
+
|
|
34
|
+
- **config**: Update pre-commit hooks
|
|
35
|
+
([`6ec7a6d`](https://github.com/justmatias/fauth/commit/6ec7a6de9c618d1d5bf8b0f157d52888346f49a1))
|
|
36
|
+
|
|
37
|
+
- **config**: Update requirements.txt
|
|
38
|
+
([`862978f`](https://github.com/justmatias/fauth/commit/862978f8073c065086b3080dafc45e3b604b98ce))
|
|
39
|
+
|
|
40
|
+
- **deps**: Bump cryptography from 46.0.5 to 46.0.6
|
|
41
|
+
([`a61129d`](https://github.com/justmatias/fauth/commit/a61129df6777736ecff5df4c1a878a70b378025d))
|
|
42
|
+
|
|
43
|
+
Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.5 to 46.0.6. -
|
|
44
|
+
[Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) -
|
|
45
|
+
[Commits](https://github.com/pyca/cryptography/compare/46.0.5...46.0.6)
|
|
46
|
+
|
|
47
|
+
--- updated-dependencies: - dependency-name: cryptography dependency-version: 46.0.6
|
|
48
|
+
|
|
49
|
+
dependency-type: indirect ...
|
|
50
|
+
|
|
51
|
+
Signed-off-by: dependabot[bot] <support@github.com>
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## v0.2.1 (2026-04-03)
|
|
55
|
+
|
|
56
|
+
### Bug Fixes
|
|
57
|
+
|
|
58
|
+
- Implement AuthProvider for FastAPI authentication and add testing utilities
|
|
59
|
+
([`f19f7ad`](https://github.com/justmatias/fauth/commit/f19f7ad32a8a79368d50862ffa3f4446dcc2cc70))
|
|
60
|
+
|
|
61
|
+
### Chores
|
|
62
|
+
|
|
63
|
+
- **config**: Merge with main
|
|
64
|
+
([`d8caae2`](https://github.com/justmatias/fauth/commit/d8caae2b3e357fcc2b3bc5f78c2929c1d0b12427))
|
|
65
|
+
|
|
66
|
+
|
|
4
67
|
## v0.2.0 (2026-04-02)
|
|
5
68
|
|
|
6
69
|
### Bug Fixes
|
|
@@ -32,6 +95,9 @@
|
|
|
32
95
|
|
|
33
96
|
### Bug Fixes
|
|
34
97
|
|
|
98
|
+
- **config**: Add authenticate method to AuthProvider with IdentityLoader support
|
|
99
|
+
([`f905766`](https://github.com/justmatias/fauth/commit/f905766afa02394a28f97b319e52134576c4c614))
|
|
100
|
+
|
|
35
101
|
- **config**: Add openapi security scheme support to AuthProvider and router dependencies
|
|
36
102
|
([`72d7754`](https://github.com/justmatias/fauth/commit/72d7754fb0519be979b6b00b5f253b245770698c))
|
|
37
103
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fauth
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Ergonomic, lightweight JWT authentication for FastAPI. Secure your routes instantly with plug-and-play dependency injection
|
|
5
5
|
Project-URL: Homepage, https://github.com/justmatias/fauth
|
|
6
6
|
Project-URL: Repository, https://github.com/justmatias/fauth
|
|
@@ -37,6 +37,7 @@ An ergonomic, plug-and-play authentication library for FastAPI.
|
|
|
37
37
|
|
|
38
38
|
`fauth` eliminates boilerplate around JWT, password hashing, user fetching, and Role-Based Access Control (RBAC) by leveraging FastAPI's Dependency Injection (`Depends`), Pydantic models, and Python Protocols.
|
|
39
39
|
|
|
40
|
+
[](https://github.com/justmatias/fauth/actions/workflows/dependabot/dependabot-updates)
|
|
40
41
|
[](https://pypi.org/project/fauth/)
|
|
41
42
|
[](https://pypi.org/project/fauth/)
|
|
42
43
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -78,26 +79,37 @@ from pydantic import BaseModel
|
|
|
78
79
|
class User(BaseModel):
|
|
79
80
|
id: str
|
|
80
81
|
username: str
|
|
82
|
+
hashed_password: str
|
|
81
83
|
is_active: bool = True
|
|
82
84
|
roles: list[str] = []
|
|
83
85
|
permissions: list[str] = []
|
|
84
86
|
```
|
|
85
87
|
|
|
86
|
-
### 2. Implement the `UserLoader`
|
|
88
|
+
### 2. Implement the `UserLoader` and `IdentityLoader` protocols
|
|
87
89
|
|
|
88
|
-
FAuth uses a callback-based approach
|
|
90
|
+
FAuth uses a callback-based approach. You provide functions that retrieve users from your data source:
|
|
89
91
|
|
|
90
92
|
```python
|
|
91
|
-
from fauth import TokenPayload
|
|
93
|
+
from fauth import TokenPayload, hash_password
|
|
92
94
|
|
|
93
95
|
# Your database, ORM, or any data source
|
|
94
96
|
DB: dict[str, User] = {
|
|
95
|
-
"user-123": User(
|
|
97
|
+
"user-123": User(
|
|
98
|
+
id="user-123",
|
|
99
|
+
username="alice",
|
|
100
|
+
hashed_password=hash_password("s3cret"),
|
|
101
|
+
roles=["admin"],
|
|
102
|
+
permissions=["read", "write"],
|
|
103
|
+
),
|
|
96
104
|
}
|
|
97
105
|
|
|
106
|
+
# UserLoader — resolves a user from a decoded JWT
|
|
98
107
|
async def load_user(payload: TokenPayload) -> User | None:
|
|
99
|
-
"""Look up a user by the `sub` claim from the JWT."""
|
|
100
108
|
return DB.get(payload.sub)
|
|
109
|
+
|
|
110
|
+
# IdentityLoader — resolves a user by identifier (for password authentication)
|
|
111
|
+
async def load_identity(identifier: str) -> User | None:
|
|
112
|
+
return next((u for u in DB.values() if u.username == identifier), None)
|
|
101
113
|
```
|
|
102
114
|
|
|
103
115
|
### 3. Create the AuthProvider
|
|
@@ -106,7 +118,11 @@ async def load_user(payload: TokenPayload) -> User | None:
|
|
|
106
118
|
from fauth import AuthConfig, AuthProvider
|
|
107
119
|
|
|
108
120
|
config = AuthConfig(secret_key="my-super-secret-key")
|
|
109
|
-
auth: AuthProvider[User] = AuthProvider(
|
|
121
|
+
auth: AuthProvider[User] = AuthProvider(
|
|
122
|
+
config=config,
|
|
123
|
+
user_loader=load_user,
|
|
124
|
+
identity_loader=load_identity,
|
|
125
|
+
)
|
|
110
126
|
```
|
|
111
127
|
|
|
112
128
|
### 4. Wire it into FastAPI
|
|
@@ -117,24 +133,26 @@ from fastapi import FastAPI, Depends
|
|
|
117
133
|
app = FastAPI()
|
|
118
134
|
|
|
119
135
|
@app.post("/login")
|
|
120
|
-
async def login():
|
|
121
|
-
|
|
136
|
+
async def login(username: str, password: str):
|
|
137
|
+
user = await auth.authenticate(username, password)
|
|
138
|
+
return await auth.login(sub=user.id)
|
|
122
139
|
|
|
123
140
|
@app.get("/me")
|
|
124
141
|
async def get_me(user: User = Depends(auth.require_user)):
|
|
125
142
|
return {"message": f"Hello {user.username}"}
|
|
126
143
|
```
|
|
127
144
|
|
|
128
|
-
That's it. The `/me` endpoint is
|
|
145
|
+
That's it. The `/login` endpoint verifies credentials via `authenticate()`, then issues tokens via `login()`. The `/me` endpoint is protected — requests without a valid `Bearer` token will receive a `401 Unauthorized` response.
|
|
129
146
|
|
|
130
147
|
---
|
|
131
148
|
|
|
132
149
|
## Full Example
|
|
133
150
|
|
|
134
151
|
```python
|
|
135
|
-
from fastapi import FastAPI, Depends
|
|
152
|
+
from fastapi import FastAPI, Depends, HTTPException
|
|
153
|
+
from fastapi.security import OAuth2PasswordRequestForm
|
|
136
154
|
from pydantic import BaseModel
|
|
137
|
-
from fauth import AuthConfig, AuthProvider, TokenPayload, SecureAPIRouter
|
|
155
|
+
from fauth import AuthConfig, AuthProvider, TokenPayload, SecureAPIRouter, hash_password
|
|
138
156
|
|
|
139
157
|
app = FastAPI()
|
|
140
158
|
|
|
@@ -142,43 +160,64 @@ app = FastAPI()
|
|
|
142
160
|
class User(BaseModel):
|
|
143
161
|
id: str
|
|
144
162
|
username: str
|
|
163
|
+
hashed_password: str
|
|
145
164
|
is_active: bool = True
|
|
146
165
|
roles: list[str] = []
|
|
147
166
|
permissions: list[str] = []
|
|
148
167
|
|
|
149
|
-
# Mock
|
|
168
|
+
# Mock databases
|
|
150
169
|
DB: dict[str, User] = {
|
|
151
|
-
"user-123": User(
|
|
170
|
+
"user-123": User(
|
|
171
|
+
id="user-123",
|
|
172
|
+
username="alice",
|
|
173
|
+
hashed_password=hash_password("s3cret"),
|
|
174
|
+
roles=["admin"],
|
|
175
|
+
permissions=["read", "write"],
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# Identity lookup by username (used by authenticate)
|
|
180
|
+
IDENTITY_DB: dict[str, User] = {
|
|
181
|
+
"alice": DB["user-123"],
|
|
152
182
|
}
|
|
153
183
|
|
|
154
184
|
# 2. Define the callback that retrieves a user from the decoded JWT
|
|
155
185
|
async def load_user(payload: TokenPayload) -> User | None:
|
|
156
186
|
return DB.get(payload.sub)
|
|
157
187
|
|
|
158
|
-
# 3.
|
|
188
|
+
# 3. Define the callback that retrieves a user by identifier (for password auth)
|
|
189
|
+
async def load_identity(identifier: str) -> User | None:
|
|
190
|
+
return IDENTITY_DB.get(identifier)
|
|
191
|
+
|
|
192
|
+
# 4. Instantiate the auth component
|
|
159
193
|
config = AuthConfig(secret_key="my-super-secret-key", algorithm="HS256")
|
|
160
|
-
auth: AuthProvider[User] = AuthProvider(
|
|
194
|
+
auth: AuthProvider[User] = AuthProvider(
|
|
195
|
+
config=config,
|
|
196
|
+
user_loader=load_user,
|
|
197
|
+
identity_loader=load_identity,
|
|
198
|
+
)
|
|
161
199
|
|
|
162
200
|
# --- Routes ---
|
|
163
201
|
|
|
164
202
|
@app.post("/login")
|
|
165
|
-
async def login():
|
|
166
|
-
#
|
|
167
|
-
|
|
203
|
+
async def login(username: str, password: str):
|
|
204
|
+
# 5. Verify credentials, then issue tokens
|
|
205
|
+
user = await auth.authenticate(username, password)
|
|
206
|
+
return await auth.login(sub=user.id)
|
|
168
207
|
|
|
169
208
|
@app.get("/me")
|
|
170
209
|
async def get_me(user: User = Depends(auth.require_user)):
|
|
171
|
-
#
|
|
210
|
+
# 6. `auth.require_user` secures the endpoint automatically
|
|
172
211
|
return {"message": f"Hello {user.username}"}
|
|
173
212
|
|
|
174
213
|
@app.get("/admin")
|
|
175
214
|
async def get_admin_data(user: User = Depends(auth.require_roles(["admin"]))):
|
|
176
|
-
#
|
|
215
|
+
# 7. `auth.require_roles` enforces RBAC with list of roles
|
|
177
216
|
return {"secret_data": "Top secret admin info"}
|
|
178
217
|
|
|
179
218
|
# --- Securing Multiple Routes ---
|
|
180
219
|
|
|
181
|
-
#
|
|
220
|
+
# 8. Use `SecureAPIRouter` to protect an entire group of routes.
|
|
182
221
|
# Any route added to this router will require an active user automatically.
|
|
183
222
|
# This also enables the "Authorize" button in Swagger UI!
|
|
184
223
|
secure_router = SecureAPIRouter(auth_provider=auth, prefix="/internal", tags=["Protected"])
|
|
@@ -243,21 +282,24 @@ The main orchestrator. Provides FastAPI dependencies for authentication and auth
|
|
|
243
282
|
AuthProvider(
|
|
244
283
|
config: AuthConfig,
|
|
245
284
|
user_loader: UserLoader[T],
|
|
246
|
-
|
|
285
|
+
identity_loader: IdentityLoader[T] | None = None, # Required for authenticate()
|
|
286
|
+
transport: Transport | None = None, # Defaults to BearerTransport()
|
|
247
287
|
token_payload_schema: type[TokenPayload] = TokenPayload,
|
|
288
|
+
password_field_name: str = "hashed_password", # Attribute on user model holding the hash
|
|
248
289
|
)
|
|
249
290
|
```
|
|
250
291
|
|
|
251
292
|
#### Methods
|
|
252
293
|
|
|
253
|
-
| Method
|
|
254
|
-
|
|
|
255
|
-
| `require_user`
|
|
256
|
-
| `require_active_user`
|
|
257
|
-
| `require_roles(roles)`
|
|
258
|
-
| `require_permissions(perms)`
|
|
259
|
-
| `
|
|
260
|
-
| `
|
|
294
|
+
| Method | Returns | Description |
|
|
295
|
+
| ----------------------------------- | --------------- | ------------------------------------------------------------------------ |
|
|
296
|
+
| `require_user` | `T` | FastAPI dependency — extracts and validates the token, loads the user |
|
|
297
|
+
| `require_active_user` | `T` | Like `require_user`, but also checks `user.is_active` |
|
|
298
|
+
| `require_roles(roles)` | `Callable` | Returns a dependency that demands the user has all specified roles |
|
|
299
|
+
| `require_permissions(perms)` | `Callable` | Returns a dependency that demands the user has all specified permissions |
|
|
300
|
+
| `authenticate(identifier, password)`| `T` | Verifies credentials via `IdentityLoader` + password check |
|
|
301
|
+
| `login(sub, scopes?, extra?)` | `TokenResponse` | Issues access + refresh tokens for a given subject |
|
|
302
|
+
| `get_security_scheme()` | `SecurityBase` | Returns the OpenAPI security scheme for docs |
|
|
261
303
|
|
|
262
304
|
### `UserLoader` Protocol
|
|
263
305
|
|
|
@@ -279,6 +321,24 @@ class MyUserLoader:
|
|
|
279
321
|
return await self.db.get_user(token_payload.sub)
|
|
280
322
|
```
|
|
281
323
|
|
|
324
|
+
### `IdentityLoader` Protocol
|
|
325
|
+
|
|
326
|
+
Used by `authenticate()` to look up a user by an identifier (username, email, etc.):
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
# As a plain function
|
|
330
|
+
async def load_identity(identifier: str) -> User | None:
|
|
331
|
+
return await db.get_user_by_username(identifier)
|
|
332
|
+
|
|
333
|
+
# Or as a callable class
|
|
334
|
+
class MyIdentityLoader:
|
|
335
|
+
def __init__(self, db: Database):
|
|
336
|
+
self.db = db
|
|
337
|
+
|
|
338
|
+
async def __call__(self, identifier: str) -> User | None:
|
|
339
|
+
return await self.db.get_user_by_username(identifier)
|
|
340
|
+
```
|
|
341
|
+
|
|
282
342
|
### `TokenPayload`
|
|
283
343
|
|
|
284
344
|
The decoded JWT structure. Accepts extra claims via `model_config = ConfigDict(extra="allow")`.
|
|
@@ -349,6 +409,78 @@ is_valid = verify_password("my-password", hashed) # True
|
|
|
349
409
|
|
|
350
410
|
---
|
|
351
411
|
|
|
412
|
+
## Authentication with Password Verification
|
|
413
|
+
|
|
414
|
+
FAuth provides a built-in `authenticate()` method on `AuthProvider` that handles credential verification using the `IdentityLoader` protocol and Argon2 password hashing.
|
|
415
|
+
|
|
416
|
+
### Basic setup
|
|
417
|
+
|
|
418
|
+
```python
|
|
419
|
+
from pydantic import BaseModel
|
|
420
|
+
from fauth import AuthConfig, AuthProvider, hash_password
|
|
421
|
+
|
|
422
|
+
class User(BaseModel):
|
|
423
|
+
id: str
|
|
424
|
+
username: str
|
|
425
|
+
hashed_password: str
|
|
426
|
+
is_active: bool = True
|
|
427
|
+
|
|
428
|
+
# Identity loader retrieves user by username/email/etc.
|
|
429
|
+
async def load_identity(identifier: str) -> User | None:
|
|
430
|
+
return await db.get_user_by_username(identifier)
|
|
431
|
+
|
|
432
|
+
# Token-based user loader (used by require_user)
|
|
433
|
+
async def load_user(payload) -> User | None:
|
|
434
|
+
return await db.get_user_by_id(payload.sub)
|
|
435
|
+
|
|
436
|
+
auth = AuthProvider(
|
|
437
|
+
config=AuthConfig(secret_key="my-secret"),
|
|
438
|
+
user_loader=load_user,
|
|
439
|
+
identity_loader=load_identity,
|
|
440
|
+
)
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Using `authenticate()` in a login endpoint
|
|
444
|
+
|
|
445
|
+
```python
|
|
446
|
+
from fastapi import FastAPI
|
|
447
|
+
|
|
448
|
+
app = FastAPI()
|
|
449
|
+
|
|
450
|
+
@app.post("/login")
|
|
451
|
+
async def login(username: str, password: str):
|
|
452
|
+
# Verifies password and checks is_active
|
|
453
|
+
user = await auth.authenticate(username, password)
|
|
454
|
+
# Issue tokens
|
|
455
|
+
return await auth.login(sub=user.id)
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
`authenticate()` performs three checks in order:
|
|
459
|
+
1. **User exists** — looks up the user via `IdentityLoader`. Raises `401` if not found.
|
|
460
|
+
2. **Password is valid** — verifies the plain password against the hashed password stored on the user. Raises `401` if invalid.
|
|
461
|
+
3. **User is active** — checks `user.is_active` (if the attribute exists). Raises `401` if inactive.
|
|
462
|
+
|
|
463
|
+
### Custom password field
|
|
464
|
+
|
|
465
|
+
By default, `authenticate()` reads the hash from `user.hashed_password`. If your model stores it under a different attribute name, pass `password_field_name`:
|
|
466
|
+
|
|
467
|
+
```python
|
|
468
|
+
class User(BaseModel):
|
|
469
|
+
id: str
|
|
470
|
+
pw_hash: str # non-standard field name
|
|
471
|
+
|
|
472
|
+
auth = AuthProvider(
|
|
473
|
+
config=config,
|
|
474
|
+
user_loader=load_user,
|
|
475
|
+
identity_loader=load_identity,
|
|
476
|
+
password_field_name="pw_hash",
|
|
477
|
+
)
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
> **Note:** If `AuthProvider` is created without an `identity_loader`, calling `authenticate()` will raise a `RuntimeError`.
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
352
484
|
## Custom Token Payload
|
|
353
485
|
|
|
354
486
|
If you need custom claims in your tokens (e.g., `tenant_id`, `organization_id`), subclass `TokenPayload` and pass it to `AuthProvider`:
|
|
@@ -535,11 +667,12 @@ async def test_secure_me_endpoint():
|
|
|
535
667
|
|
|
536
668
|
### Testing utilities reference
|
|
537
669
|
|
|
538
|
-
| Import | Description
|
|
539
|
-
| ----------------------------------------------------- |
|
|
540
|
-
| `build_fake_auth_provider(users?, config_overrides?)` | Creates an `AuthProvider` backed by in-memory fakes
|
|
541
|
-
| `fake_auth_config(**overrides)` | Returns an `AuthConfig` with safe test defaults
|
|
542
|
-
| `FakeUserLoader[T]` | In-memory `UserLoader` — populate with `.add_user(id, user)`
|
|
670
|
+
| Import | Description |
|
|
671
|
+
| ----------------------------------------------------- | --------------------------------------------------------------------- |
|
|
672
|
+
| `build_fake_auth_provider(users?, config_overrides?)` | Creates an `AuthProvider` backed by in-memory fakes |
|
|
673
|
+
| `fake_auth_config(**overrides)` | Returns an `AuthConfig` with safe test defaults |
|
|
674
|
+
| `FakeUserLoader[T]` | In-memory `UserLoader` — populate with `.add_user(id, user)` |
|
|
675
|
+
| `FakeIdentityLoader[T]` | In-memory `IdentityLoader` — populate with `.add_user(id, user)` |
|
|
543
676
|
|
|
544
677
|
---
|
|
545
678
|
|
|
@@ -612,7 +745,9 @@ FAuth raises `HTTPException` with standard HTTP status codes:
|
|
|
612
745
|
| Expired token | `401` | `"Token expired"` |
|
|
613
746
|
| Invalid/malformed token | `401` | `"Invalid token"` |
|
|
614
747
|
| User not found | `401` | `"User does not exist"` |
|
|
615
|
-
|
|
|
748
|
+
| Invalid credentials | `401` | `"Invalid credentials"` (from `authenticate()`) |
|
|
749
|
+
| Inactive user (token) | `400` | `"Inactive user"` (from `require_active_user`) |
|
|
750
|
+
| Inactive user (login) | `401` | `"Inactive user"` (from `authenticate()`) |
|
|
616
751
|
| Missing role | `403` | `"Missing role: {role}"` |
|
|
617
752
|
| Missing permission | `403` | `"Insufficient permissions: requires {permission} permission"` |
|
|
618
753
|
|