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.
Files changed (40) hide show
  1. fastapi_multiauth-1.0.0/PKG-INFO +142 -0
  2. fastapi_multiauth-1.0.0/README.md +101 -0
  3. {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/pyproject.toml +24 -4
  4. {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/pyproject.toml.orig +18 -10
  5. fastapi_multiauth-1.0.0/src/fastapi_multiauth/__init__.py +81 -0
  6. {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/_http.py +33 -8
  7. fastapi_multiauth-1.0.0/src/fastapi_multiauth/abc.py +665 -0
  8. fastapi_multiauth-1.0.0/src/fastapi_multiauth/amr.py +55 -0
  9. fastapi_multiauth-1.0.0/src/fastapi_multiauth/exceptions.py +130 -0
  10. fastapi_multiauth-1.0.0/src/fastapi_multiauth/introspection.py +289 -0
  11. fastapi_multiauth-1.0.0/src/fastapi_multiauth/jwt.py +379 -0
  12. {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/oauth/__init__.py +14 -5
  13. {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/oauth/client.py +52 -55
  14. fastapi_multiauth-1.0.0/src/fastapi_multiauth/oauth/id_token.py +139 -0
  15. fastapi_multiauth-1.0.0/src/fastapi_multiauth/oauth/utils.py +194 -0
  16. {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/sources/__init__.py +4 -0
  17. {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/sources/basic.py +19 -19
  18. {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/sources/bearer.py +50 -31
  19. fastapi_multiauth-1.0.0/src/fastapi_multiauth/sources/cookie.py +485 -0
  20. fastapi_multiauth-1.0.0/src/fastapi_multiauth/sources/dpop.py +318 -0
  21. {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/sources/header.py +7 -10
  22. {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/sources/multi.py +90 -22
  23. {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/sources/oauth2.py +42 -24
  24. {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/sources/query.py +10 -14
  25. fastapi_multiauth-1.0.0/src/fastapi_multiauth/sources/signature.py +277 -0
  26. {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/surface.py +41 -41
  27. fastapi_multiauth-1.0.0/src/fastapi_multiauth/totp.py +133 -0
  28. fastapi_multiauth-1.0.0/src/fastapi_multiauth/utils.py +401 -0
  29. fastapi_multiauth-0.7.0/PKG-INFO +0 -174
  30. fastapi_multiauth-0.7.0/README.md +0 -133
  31. fastapi_multiauth-0.7.0/src/fastapi_multiauth/__init__.py +0 -53
  32. fastapi_multiauth-0.7.0/src/fastapi_multiauth/abc.py +0 -486
  33. fastapi_multiauth-0.7.0/src/fastapi_multiauth/exceptions.py +0 -97
  34. fastapi_multiauth-0.7.0/src/fastapi_multiauth/jwt.py +0 -265
  35. fastapi_multiauth-0.7.0/src/fastapi_multiauth/oauth/utils.py +0 -165
  36. fastapi_multiauth-0.7.0/src/fastapi_multiauth/sources/cookie.py +0 -253
  37. fastapi_multiauth-0.7.0/src/fastapi_multiauth/utils.py +0 -120
  38. {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/LICENSE +0 -0
  39. {fastapi_multiauth-0.7.0 → fastapi_multiauth-1.0.0}/src/fastapi_multiauth/_imports.py +0 -0
  40. {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
+ [![CI](https://github.com/d3vyce/fastapi-multiauth/actions/workflows/ci.yml/badge.svg)](https://github.com/d3vyce/fastapi-multiauth/actions/workflows/ci.yml)
47
+ [![codecov](https://codecov.io/gh/d3vyce/fastapi-multiauth/graph/badge.svg)](https://codecov.io/gh/d3vyce/fastapi-multiauth)
48
+ [![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/d3vyce/fastapi-multiauth?utm_source=badge)
49
+ [![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)
50
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
51
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
52
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
53
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![CI](https://github.com/d3vyce/fastapi-multiauth/actions/workflows/ci.yml/badge.svg)](https://github.com/d3vyce/fastapi-multiauth/actions/workflows/ci.yml)
6
+ [![codecov](https://codecov.io/gh/d3vyce/fastapi-multiauth/graph/badge.svg)](https://codecov.io/gh/d3vyce/fastapi-multiauth)
7
+ [![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/d3vyce/fastapi-multiauth?utm_source=badge)
8
+ [![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)
9
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
10
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
11
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
12
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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.7.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 :: 4 - Beta",
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.30",
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.7.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 :: 4 - Beta",
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.30",
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 security rules, scoped to the library source.
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 non-string URLs and URLs that would send credentials over plaintext HTTP."""
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 _get_json(
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
- """GET *url* and decode the JSON body, mapping failures to *error_cls*."""
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.get(url, headers=headers)
45
+ resp = await http.request(method, url, headers=headers, data=data)
45
46
  resp.raise_for_status()
46
- return resp.json()
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 json.JSONDecodeError as exc:
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
+ )