fastapi-multiauth 0.7.0__tar.gz → 1.0.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.
- fastapi_multiauth-1.0.0/PKG-INFO +142 -0
- fastapi_multiauth-1.0.0/README.md +101 -0
- {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/pyproject.toml +24 -4
- {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/pyproject.toml.orig +18 -10
- fastapi_multiauth-1.0.0/src/fastapi_multiauth/__init__.py +81 -0
- {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/_http.py +33 -8
- fastapi_multiauth-1.0.0/src/fastapi_multiauth/abc.py +665 -0
- fastapi_multiauth-1.0.0/src/fastapi_multiauth/amr.py +55 -0
- fastapi_multiauth-1.0.0/src/fastapi_multiauth/exceptions.py +130 -0
- fastapi_multiauth-1.0.0/src/fastapi_multiauth/introspection.py +289 -0
- fastapi_multiauth-1.0.0/src/fastapi_multiauth/jwt.py +379 -0
- {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/oauth/__init__.py +14 -5
- {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/oauth/client.py +52 -55
- fastapi_multiauth-1.0.0/src/fastapi_multiauth/oauth/id_token.py +139 -0
- fastapi_multiauth-1.0.0/src/fastapi_multiauth/oauth/utils.py +194 -0
- {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/sources/__init__.py +4 -0
- {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/sources/basic.py +19 -19
- {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/sources/bearer.py +50 -31
- fastapi_multiauth-1.0.0/src/fastapi_multiauth/sources/cookie.py +485 -0
- fastapi_multiauth-1.0.0/src/fastapi_multiauth/sources/dpop.py +318 -0
- {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/sources/header.py +7 -10
- {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/sources/multi.py +90 -22
- {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/sources/oauth2.py +42 -24
- {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/sources/query.py +10 -14
- fastapi_multiauth-1.0.0/src/fastapi_multiauth/sources/signature.py +277 -0
- {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/surface.py +41 -41
- fastapi_multiauth-1.0.0/src/fastapi_multiauth/totp.py +133 -0
- fastapi_multiauth-1.0.0/src/fastapi_multiauth/utils.py +401 -0
- fastapi_multiauth-0.7.0/PKG-INFO +0 -174
- fastapi_multiauth-0.7.0/README.md +0 -133
- fastapi_multiauth-0.7.0/src/fastapi_multiauth/__init__.py +0 -53
- fastapi_multiauth-0.7.0/src/fastapi_multiauth/abc.py +0 -486
- fastapi_multiauth-0.7.0/src/fastapi_multiauth/exceptions.py +0 -97
- fastapi_multiauth-0.7.0/src/fastapi_multiauth/jwt.py +0 -265
- fastapi_multiauth-0.7.0/src/fastapi_multiauth/oauth/utils.py +0 -165
- fastapi_multiauth-0.7.0/src/fastapi_multiauth/sources/cookie.py +0 -253
- fastapi_multiauth-0.7.0/src/fastapi_multiauth/utils.py +0 -120
- {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/LICENSE +0 -0
- {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/_imports.py +0 -0
- {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/py.typed +0 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-multiauth
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Composable authentication sources for FastAPI: bring your own validator, combine multiple schemes, correct OpenAPI by construction
|
|
5
|
+
Keywords: fastapi,authentication,security,oauth,api-key,bearer,cookie
|
|
6
|
+
Author: d3vyce
|
|
7
|
+
Author-email: d3vyce <contact@d3vyce.fr>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: Framework :: FastAPI
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Session
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Dist: anyio>=3.7
|
|
27
|
+
Requires-Dist: async-lru>=2.0
|
|
28
|
+
Requires-Dist: fastapi>=0.123.0,!=0.137.0,!=0.137.1
|
|
29
|
+
Requires-Dist: httpx>=0.25.0
|
|
30
|
+
Requires-Dist: itsdangerous>=2.1
|
|
31
|
+
Requires-Dist: pyjwt[crypto]>=2.13.0 ; extra == 'jwt'
|
|
32
|
+
Requires-Dist: httpx-oauth>=0.16 ; extra == 'oauth'
|
|
33
|
+
Requires-Python: >=3.11
|
|
34
|
+
Project-URL: Homepage, https://github.com/d3vyce/fastapi-multiauth
|
|
35
|
+
Project-URL: Documentation, https://fastapi-multiauth.d3vyce.fr/
|
|
36
|
+
Project-URL: Repository, https://github.com/d3vyce/fastapi-multiauth
|
|
37
|
+
Project-URL: Issues, https://github.com/d3vyce/fastapi-multiauth/issues
|
|
38
|
+
Provides-Extra: jwt
|
|
39
|
+
Provides-Extra: oauth
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# FastAPI MultiAuth
|
|
43
|
+
|
|
44
|
+
Composable authentication sources for FastAPI: bring your own validator, combine multiple schemes with `MultiAuth`, and get correct OpenAPI documentation by construction.
|
|
45
|
+
|
|
46
|
+
[](https://github.com/d3vyce/fastapi-multiauth/actions/workflows/ci.yml)
|
|
47
|
+
[](https://codecov.io/gh/d3vyce/fastapi-multiauth)
|
|
48
|
+
[](https://codspeed.io/d3vyce/fastapi-multiauth?utm_source=badge)
|
|
49
|
+
[](https://github.com/astral-sh/ty)
|
|
50
|
+
[](https://github.com/astral-sh/uv)
|
|
51
|
+
[](https://github.com/astral-sh/ruff)
|
|
52
|
+
[](https://www.python.org/downloads/)
|
|
53
|
+
[](https://opensource.org/licenses/MIT)
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
**Documentation**: [https://fastapi-multiauth.d3vyce.fr](https://fastapi-multiauth.d3vyce.fr)
|
|
58
|
+
|
|
59
|
+
**Source Code**: [https://github.com/d3vyce/fastapi-multiauth](https://github.com/d3vyce/fastapi-multiauth)
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
You write the function that decides who a credential belongs to; the library extracts the credential, combines schemes, and declares them in OpenAPI. No user model, no database coupling, no ready-made login routes.
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
uv add fastapi-multiauth
|
|
69
|
+
uv add "fastapi-multiauth[jwt]" # JWTValidator, HTTPDPoPAuth
|
|
70
|
+
uv add "fastapi-multiauth[oauth]" # OAuth 2.0 / OIDC login helpers
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Quick start
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import os
|
|
77
|
+
|
|
78
|
+
from fastapi import FastAPI, Security
|
|
79
|
+
|
|
80
|
+
from fastapi_multiauth import (
|
|
81
|
+
APIKeyCookieAuth,
|
|
82
|
+
HTTPBearerAuth,
|
|
83
|
+
MultiAuth,
|
|
84
|
+
UnauthorizedError,
|
|
85
|
+
hash_token,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
SECRET_KEY = os.environ["SECRET_KEY"]
|
|
89
|
+
USERS = {"1": {"id": "1", "name": "alice"}}
|
|
90
|
+
TOKENS = {hash_token("user_alice-token"): "1"}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def validate_token(token: str) -> dict:
|
|
94
|
+
user_id = TOKENS.get(hash_token(token))
|
|
95
|
+
if user_id is None:
|
|
96
|
+
raise UnauthorizedError()
|
|
97
|
+
return USERS[user_id]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def validate_session(user_id: str) -> dict:
|
|
101
|
+
if user_id not in USERS:
|
|
102
|
+
raise UnauthorizedError()
|
|
103
|
+
return USERS[user_id]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
bearer = HTTPBearerAuth(validator=validate_token, prefix="user_")
|
|
107
|
+
session = APIKeyCookieAuth(
|
|
108
|
+
name="__Host-session", validator=validate_session, secret_key=SECRET_KEY
|
|
109
|
+
)
|
|
110
|
+
auth = MultiAuth(bearer, session)
|
|
111
|
+
|
|
112
|
+
app = FastAPI()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.get("/me")
|
|
116
|
+
async def me(user: dict = Security(auth)):
|
|
117
|
+
return user
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Set `SECRET_KEY` to at least 32 random bytes, for example the output of `python -c "import secrets; print(secrets.token_urlsafe(32))"`.
|
|
121
|
+
|
|
122
|
+
## Features
|
|
123
|
+
|
|
124
|
+
- **Every standard scheme**: bearer tokens with Stripe-style prefixes, signed cookie sessions, API keys, HTTP Basic, OAuth 2.0 and OpenID Connect.
|
|
125
|
+
- **`MultiAuth`**: several schemes on one route, all documented in OpenAPI, plus `optional()` for routes that also serve anonymous callers.
|
|
126
|
+
- **Stateless sessions**: HMAC-signed cookies with expiry, key rotation, opt-in revocable session ids, and `csrf=True` to refuse cross-origin requests.
|
|
127
|
+
- **Tokens done right**: secure generation, hash-only storage, checksums, `JWTValidator` for provider JWTs (JWKS, rotation, `aud`/`iss`), `IntrospectionValidator` for opaque ones, and `HTTPDPoPAuth` for tokens bound to the client's key.
|
|
128
|
+
- **Signed webhooks**: `HMACSignatureAuth` checks the sender's signature over the raw body, with timestamp tolerance and secret rotation.
|
|
129
|
+
- **Scopes and step-up**: fail-closed scope checks, and `fresh()` / `stepped_up()` with RFC 9470 challenges for sensitive routes.
|
|
130
|
+
- **Second factor**: TOTP helpers that refuse a replayed code, and the login level signed into the session cookie.
|
|
131
|
+
- **OAuth login helpers**: discovery, CSRF `state`, PKCE, code exchange and ID token validation.
|
|
132
|
+
- **Observable**: reason-coded security logs and `auth_surface(app)` to audit what guards every route.
|
|
133
|
+
|
|
134
|
+
See the [comparison](https://fastapi-multiauth.d3vyce.fr/guide/comparison/) for how it sits next to `fastapi.security`, fastapi-users, AuthX and Authlib, and the [security model](https://fastapi-multiauth.d3vyce.fr/security-model/) for what it guarantees.
|
|
135
|
+
|
|
136
|
+
## Contributing
|
|
137
|
+
|
|
138
|
+
Contributions are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md) and the [Code of Conduct](CODE_OF_CONDUCT.md). Report vulnerabilities privately, as the [security policy](SECURITY.md) describes.
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# FastAPI MultiAuth
|
|
2
|
+
|
|
3
|
+
Composable authentication sources for FastAPI: bring your own validator, combine multiple schemes with `MultiAuth`, and get correct OpenAPI documentation by construction.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/d3vyce/fastapi-multiauth/actions/workflows/ci.yml)
|
|
6
|
+
[](https://codecov.io/gh/d3vyce/fastapi-multiauth)
|
|
7
|
+
[](https://codspeed.io/d3vyce/fastapi-multiauth?utm_source=badge)
|
|
8
|
+
[](https://github.com/astral-sh/ty)
|
|
9
|
+
[](https://github.com/astral-sh/uv)
|
|
10
|
+
[](https://github.com/astral-sh/ruff)
|
|
11
|
+
[](https://www.python.org/downloads/)
|
|
12
|
+
[](https://opensource.org/licenses/MIT)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
**Documentation**: [https://fastapi-multiauth.d3vyce.fr](https://fastapi-multiauth.d3vyce.fr)
|
|
17
|
+
|
|
18
|
+
**Source Code**: [https://github.com/d3vyce/fastapi-multiauth](https://github.com/d3vyce/fastapi-multiauth)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
You write the function that decides who a credential belongs to; the library extracts the credential, combines schemes, and declares them in OpenAPI. No user model, no database coupling, no ready-made login routes.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv add fastapi-multiauth
|
|
28
|
+
uv add "fastapi-multiauth[jwt]" # JWTValidator, HTTPDPoPAuth
|
|
29
|
+
uv add "fastapi-multiauth[oauth]" # OAuth 2.0 / OIDC login helpers
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import os
|
|
36
|
+
|
|
37
|
+
from fastapi import FastAPI, Security
|
|
38
|
+
|
|
39
|
+
from fastapi_multiauth import (
|
|
40
|
+
APIKeyCookieAuth,
|
|
41
|
+
HTTPBearerAuth,
|
|
42
|
+
MultiAuth,
|
|
43
|
+
UnauthorizedError,
|
|
44
|
+
hash_token,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
SECRET_KEY = os.environ["SECRET_KEY"]
|
|
48
|
+
USERS = {"1": {"id": "1", "name": "alice"}}
|
|
49
|
+
TOKENS = {hash_token("user_alice-token"): "1"}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def validate_token(token: str) -> dict:
|
|
53
|
+
user_id = TOKENS.get(hash_token(token))
|
|
54
|
+
if user_id is None:
|
|
55
|
+
raise UnauthorizedError()
|
|
56
|
+
return USERS[user_id]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def validate_session(user_id: str) -> dict:
|
|
60
|
+
if user_id not in USERS:
|
|
61
|
+
raise UnauthorizedError()
|
|
62
|
+
return USERS[user_id]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
bearer = HTTPBearerAuth(validator=validate_token, prefix="user_")
|
|
66
|
+
session = APIKeyCookieAuth(
|
|
67
|
+
name="__Host-session", validator=validate_session, secret_key=SECRET_KEY
|
|
68
|
+
)
|
|
69
|
+
auth = MultiAuth(bearer, session)
|
|
70
|
+
|
|
71
|
+
app = FastAPI()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.get("/me")
|
|
75
|
+
async def me(user: dict = Security(auth)):
|
|
76
|
+
return user
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Set `SECRET_KEY` to at least 32 random bytes, for example the output of `python -c "import secrets; print(secrets.token_urlsafe(32))"`.
|
|
80
|
+
|
|
81
|
+
## Features
|
|
82
|
+
|
|
83
|
+
- **Every standard scheme**: bearer tokens with Stripe-style prefixes, signed cookie sessions, API keys, HTTP Basic, OAuth 2.0 and OpenID Connect.
|
|
84
|
+
- **`MultiAuth`**: several schemes on one route, all documented in OpenAPI, plus `optional()` for routes that also serve anonymous callers.
|
|
85
|
+
- **Stateless sessions**: HMAC-signed cookies with expiry, key rotation, opt-in revocable session ids, and `csrf=True` to refuse cross-origin requests.
|
|
86
|
+
- **Tokens done right**: secure generation, hash-only storage, checksums, `JWTValidator` for provider JWTs (JWKS, rotation, `aud`/`iss`), `IntrospectionValidator` for opaque ones, and `HTTPDPoPAuth` for tokens bound to the client's key.
|
|
87
|
+
- **Signed webhooks**: `HMACSignatureAuth` checks the sender's signature over the raw body, with timestamp tolerance and secret rotation.
|
|
88
|
+
- **Scopes and step-up**: fail-closed scope checks, and `fresh()` / `stepped_up()` with RFC 9470 challenges for sensitive routes.
|
|
89
|
+
- **Second factor**: TOTP helpers that refuse a replayed code, and the login level signed into the session cookie.
|
|
90
|
+
- **OAuth login helpers**: discovery, CSRF `state`, PKCE, code exchange and ID token validation.
|
|
91
|
+
- **Observable**: reason-coded security logs and `auth_surface(app)` to audit what guards every route.
|
|
92
|
+
|
|
93
|
+
See the [comparison](https://fastapi-multiauth.d3vyce.fr/guide/comparison/) for how it sits next to `fastapi.security`, fastapi-users, AuthX and Authlib, and the [security model](https://fastapi-multiauth.d3vyce.fr/security-model/) for what it guarantees.
|
|
94
|
+
|
|
95
|
+
## Contributing
|
|
96
|
+
|
|
97
|
+
Contributions are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md) and the [Code of Conduct](CODE_OF_CONDUCT.md). Report vulnerabilities privately, as the [security policy](SECURITY.md) describes.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fastapi-multiauth"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "1.0.0"
|
|
4
4
|
description = "Composable authentication sources for FastAPI: bring your own validator, combine multiple schemes, correct OpenAPI by construction"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -16,7 +16,7 @@ keywords = [
|
|
|
16
16
|
"cookie",
|
|
17
17
|
]
|
|
18
18
|
classifiers = [
|
|
19
|
-
"Development Status ::
|
|
19
|
+
"Development Status :: 5 - Production/Stable",
|
|
20
20
|
"Framework :: AsyncIO",
|
|
21
21
|
"Framework :: FastAPI",
|
|
22
22
|
"Intended Audience :: Developers",
|
|
@@ -66,6 +66,8 @@ dev = [
|
|
|
66
66
|
]
|
|
67
67
|
tests = [
|
|
68
68
|
"coverage>=7.0.0",
|
|
69
|
+
"httpx2>=2.16",
|
|
70
|
+
"pyotp>=2.10.0",
|
|
69
71
|
"pytest-anyio>=0.0.0",
|
|
70
72
|
"pytest-codspeed>=5.0",
|
|
71
73
|
"pytest-cov>=4.0.0",
|
|
@@ -74,33 +76,51 @@ tests = [
|
|
|
74
76
|
]
|
|
75
77
|
docs = [
|
|
76
78
|
"mkdocstrings-python>=2.0.2",
|
|
77
|
-
"zensical>=0.0.
|
|
79
|
+
"zensical>=0.0.71",
|
|
78
80
|
]
|
|
79
81
|
|
|
80
82
|
[build-system]
|
|
81
83
|
requires = ["uv_build>=0.11.21,<0.14.0"]
|
|
82
84
|
build-backend = "uv_build"
|
|
83
85
|
|
|
86
|
+
[tool.ruff]
|
|
87
|
+
extend-exclude = ["docs"]
|
|
88
|
+
|
|
84
89
|
[tool.ruff.lint]
|
|
85
90
|
extend-select = [
|
|
91
|
+
"D",
|
|
86
92
|
"S",
|
|
87
93
|
"UP",
|
|
88
94
|
]
|
|
95
|
+
ignore = [
|
|
96
|
+
"D105",
|
|
97
|
+
"D107",
|
|
98
|
+
]
|
|
89
99
|
|
|
90
100
|
[tool.ruff.lint.per-file-ignores]
|
|
91
101
|
"tests/**" = [
|
|
102
|
+
"D",
|
|
92
103
|
"S",
|
|
93
104
|
"B008",
|
|
94
105
|
]
|
|
95
106
|
"benchmarks/**" = [
|
|
107
|
+
"D",
|
|
108
|
+
"S",
|
|
109
|
+
"B008",
|
|
110
|
+
]
|
|
111
|
+
"docs_src/**" = [
|
|
112
|
+
"D",
|
|
96
113
|
"S",
|
|
97
114
|
"B008",
|
|
98
115
|
]
|
|
99
116
|
|
|
117
|
+
[tool.ruff.lint.pydocstyle]
|
|
118
|
+
convention = "google"
|
|
119
|
+
|
|
100
120
|
[tool.pytest.ini_options]
|
|
101
121
|
testpaths = ["tests"]
|
|
122
|
+
pythonpath = ["."]
|
|
102
123
|
anyio_mode = "auto"
|
|
103
|
-
filterwarnings = ["ignore::DeprecationWarning"]
|
|
104
124
|
|
|
105
125
|
[tool.coverage.run]
|
|
106
126
|
source = ["src/fastapi_multiauth"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fastapi-multiauth"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "1.0.0"
|
|
4
4
|
description = "Composable authentication sources for FastAPI: bring your own validator, combine multiple schemes, correct OpenAPI by construction"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -11,7 +11,7 @@ authors = [
|
|
|
11
11
|
]
|
|
12
12
|
keywords = ["fastapi", "authentication", "security", "oauth", "api-key", "bearer", "cookie"]
|
|
13
13
|
classifiers = [
|
|
14
|
-
"Development Status ::
|
|
14
|
+
"Development Status :: 5 - Production/Stable",
|
|
15
15
|
"Framework :: AsyncIO",
|
|
16
16
|
"Framework :: FastAPI",
|
|
17
17
|
"Intended Audience :: Developers",
|
|
@@ -61,6 +61,8 @@ dev = [
|
|
|
61
61
|
]
|
|
62
62
|
tests = [
|
|
63
63
|
"coverage>=7.0.0",
|
|
64
|
+
"httpx2>=2.16",
|
|
65
|
+
"pyotp>=2.10.0",
|
|
64
66
|
"pytest-anyio>=0.0.0",
|
|
65
67
|
"pytest-codspeed>=5.0",
|
|
66
68
|
"pytest-cov>=4.0.0",
|
|
@@ -69,27 +71,33 @@ tests = [
|
|
|
69
71
|
]
|
|
70
72
|
docs = [
|
|
71
73
|
"mkdocstrings-python>=2.0.2",
|
|
72
|
-
"zensical>=0.0.
|
|
74
|
+
"zensical>=0.0.71",
|
|
73
75
|
]
|
|
74
76
|
|
|
75
77
|
[build-system]
|
|
76
78
|
requires = ["uv_build>=0.11.21,<0.14.0"]
|
|
77
79
|
build-backend = "uv_build"
|
|
78
80
|
|
|
81
|
+
[tool.ruff]
|
|
82
|
+
extend-exclude = ["docs"]
|
|
83
|
+
|
|
79
84
|
[tool.ruff.lint]
|
|
80
|
-
# "S" = flake8-bandit
|
|
81
|
-
extend-select = ["S", "UP"]
|
|
85
|
+
# "D" = Google-style docstrings, "S" = flake8-bandit; both scoped to the library source.
|
|
86
|
+
extend-select = ["D", "S", "UP"]
|
|
87
|
+
ignore = ["D105", "D107"]
|
|
82
88
|
|
|
83
89
|
[tool.ruff.lint.per-file-ignores]
|
|
84
|
-
"tests/**" = ["S", "B008"]
|
|
85
|
-
"benchmarks/**" = ["S", "B008"]
|
|
90
|
+
"tests/**" = ["D", "S", "B008"]
|
|
91
|
+
"benchmarks/**" = ["D", "S", "B008"]
|
|
92
|
+
"docs_src/**" = ["D", "S", "B008"]
|
|
93
|
+
|
|
94
|
+
[tool.ruff.lint.pydocstyle]
|
|
95
|
+
convention = "google"
|
|
86
96
|
|
|
87
97
|
[tool.pytest.ini_options]
|
|
88
98
|
testpaths = ["tests"]
|
|
99
|
+
pythonpath = ["."]
|
|
89
100
|
anyio_mode = "auto"
|
|
90
|
-
filterwarnings = [
|
|
91
|
-
"ignore::DeprecationWarning",
|
|
92
|
-
]
|
|
93
101
|
|
|
94
102
|
[tool.coverage.run]
|
|
95
103
|
source = ["src/fastapi_multiauth"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Composable authentication sources for FastAPI.
|
|
2
|
+
|
|
3
|
+
The OAuth 2.0 / OpenID Connect login helpers live in the separate
|
|
4
|
+
`fastapi_multiauth.oauth` namespace and need the `oauth` extra:
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from fastapi_multiauth.oauth import oauth_build_authorization_redirect
|
|
8
|
+
```
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
from . import oauth
|
|
14
|
+
from .abc import AuthSource
|
|
15
|
+
from .amr import AuthenticationMethodReference
|
|
16
|
+
from .exceptions import (
|
|
17
|
+
ForbiddenError,
|
|
18
|
+
InsufficientAuthenticationError,
|
|
19
|
+
StaleCredentialError,
|
|
20
|
+
UnauthorizedError,
|
|
21
|
+
)
|
|
22
|
+
from .introspection import IntrospectionValidator
|
|
23
|
+
from .jwt import JWTValidator
|
|
24
|
+
from .sources import (
|
|
25
|
+
APIKeyCookieAuth,
|
|
26
|
+
APIKeyHeaderAuth,
|
|
27
|
+
APIKeyQueryAuth,
|
|
28
|
+
HMACSignatureAuth,
|
|
29
|
+
HTTPBasicAuth,
|
|
30
|
+
HTTPBearerAuth,
|
|
31
|
+
HTTPDPoPAuth,
|
|
32
|
+
MultiAuth,
|
|
33
|
+
OAuth2AuthorizationCodeBearerAuth,
|
|
34
|
+
OAuth2PasswordBearerAuth,
|
|
35
|
+
OpenIdConnectAuth,
|
|
36
|
+
)
|
|
37
|
+
from .surface import RouteAuth, SchemeRequirement, auth_surface
|
|
38
|
+
from .totp import totp_generate_secret, totp_provisioning_uri, totp_verify
|
|
39
|
+
from .utils import (
|
|
40
|
+
checksum_token_generator,
|
|
41
|
+
hash_token,
|
|
42
|
+
verify_token_checksum,
|
|
43
|
+
verify_token_hash,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__version__ = "1.0.0"
|
|
47
|
+
|
|
48
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"APIKeyCookieAuth",
|
|
52
|
+
"APIKeyHeaderAuth",
|
|
53
|
+
"APIKeyQueryAuth",
|
|
54
|
+
"AuthSource",
|
|
55
|
+
"AuthenticationMethodReference",
|
|
56
|
+
"ForbiddenError",
|
|
57
|
+
"HMACSignatureAuth",
|
|
58
|
+
"HTTPBasicAuth",
|
|
59
|
+
"HTTPBearerAuth",
|
|
60
|
+
"HTTPDPoPAuth",
|
|
61
|
+
"InsufficientAuthenticationError",
|
|
62
|
+
"IntrospectionValidator",
|
|
63
|
+
"JWTValidator",
|
|
64
|
+
"MultiAuth",
|
|
65
|
+
"OAuth2AuthorizationCodeBearerAuth",
|
|
66
|
+
"OAuth2PasswordBearerAuth",
|
|
67
|
+
"OpenIdConnectAuth",
|
|
68
|
+
"RouteAuth",
|
|
69
|
+
"SchemeRequirement",
|
|
70
|
+
"StaleCredentialError",
|
|
71
|
+
"UnauthorizedError",
|
|
72
|
+
"auth_surface",
|
|
73
|
+
"checksum_token_generator",
|
|
74
|
+
"hash_token",
|
|
75
|
+
"oauth",
|
|
76
|
+
"totp_generate_secret",
|
|
77
|
+
"totp_provisioning_uri",
|
|
78
|
+
"totp_verify",
|
|
79
|
+
"verify_token_checksum",
|
|
80
|
+
"verify_token_hash",
|
|
81
|
+
]
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Shared HTTP transport hygiene for the OAuth and JWT modules."""
|
|
2
2
|
|
|
3
3
|
import contextlib
|
|
4
|
-
import json
|
|
5
4
|
from typing import Any
|
|
6
5
|
from urllib.parse import urlsplit
|
|
7
6
|
|
|
@@ -14,7 +13,7 @@ DEFAULT_TIMEOUT = 10.0
|
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def _require_https(url: str, description: str) -> str:
|
|
17
|
-
"""Reject
|
|
16
|
+
"""Reject a URL that is not a string, or would send credentials over HTTP."""
|
|
18
17
|
if not isinstance(url, str):
|
|
19
18
|
raise ValueError(f"{description} must be a string URL (got {url!r})") # noqa: TRY004
|
|
20
19
|
parsed = urlsplit(url)
|
|
@@ -25,26 +24,52 @@ def _require_https(url: str, description: str) -> str:
|
|
|
25
24
|
raise ValueError(f"{description} must use https:// (got {url!r})")
|
|
26
25
|
|
|
27
26
|
|
|
28
|
-
async def
|
|
27
|
+
async def _request_json(
|
|
28
|
+
method: str,
|
|
29
29
|
url: str,
|
|
30
30
|
*,
|
|
31
31
|
timeout: float,
|
|
32
32
|
error_cls: type[Exception],
|
|
33
33
|
description: str,
|
|
34
34
|
headers: dict[str, str] | None = None,
|
|
35
|
+
data: dict[str, str] | None = None,
|
|
35
36
|
client: httpx.AsyncClient | None = None,
|
|
36
|
-
) -> Any:
|
|
37
|
-
"""
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
|
+
"""Send a request and decode the JSON object body, mapping failures to `error_cls`."""
|
|
38
39
|
async with (
|
|
39
40
|
contextlib.nullcontext(client)
|
|
40
41
|
if client is not None
|
|
41
42
|
else httpx.AsyncClient(timeout=timeout)
|
|
42
43
|
) as http:
|
|
43
44
|
try:
|
|
44
|
-
resp = await http.
|
|
45
|
+
resp = await http.request(method, url, headers=headers, data=data)
|
|
45
46
|
resp.raise_for_status()
|
|
46
|
-
|
|
47
|
+
body = resp.json()
|
|
47
48
|
except httpx.HTTPError as exc:
|
|
48
49
|
raise error_cls(f"failed to fetch {description}: {exc}") from exc
|
|
49
|
-
except
|
|
50
|
+
except ValueError as exc:
|
|
50
51
|
raise error_cls(f"{description} is not valid JSON") from exc
|
|
52
|
+
if not isinstance(body, dict):
|
|
53
|
+
raise error_cls(f"{description} is not a JSON object")
|
|
54
|
+
return body
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def _get_json(
|
|
58
|
+
url: str,
|
|
59
|
+
*,
|
|
60
|
+
timeout: float,
|
|
61
|
+
error_cls: type[Exception],
|
|
62
|
+
description: str,
|
|
63
|
+
headers: dict[str, str] | None = None,
|
|
64
|
+
client: httpx.AsyncClient | None = None,
|
|
65
|
+
) -> dict[str, Any]:
|
|
66
|
+
"""GET `url` and decode the JSON object body, mapping failures to `error_cls`."""
|
|
67
|
+
return await _request_json(
|
|
68
|
+
"GET",
|
|
69
|
+
url,
|
|
70
|
+
timeout=timeout,
|
|
71
|
+
error_cls=error_cls,
|
|
72
|
+
description=description,
|
|
73
|
+
headers=headers,
|
|
74
|
+
client=client,
|
|
75
|
+
)
|