fastapi-identity-model 0.2.1__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.
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/.gitignore +14 -0
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/CHANGELOG.md +11 -3
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/PKG-INFO +5 -5
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/fastapi_identity_model/middleware.py +35 -9
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/pyproject.toml +10 -9
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/tests/test_middleware.py +31 -1
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/README.md +0 -0
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/fastapi_identity_model/__init__.py +0 -0
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/fastapi_identity_model/config.py +0 -0
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/fastapi_identity_model/dependencies.py +0 -0
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/fastapi_identity_model/py.typed +0 -0
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/fastapi_identity_model/rp.py +0 -0
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/fastapi_identity_model/token_manager.py +0 -0
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/tests/test_config.py +0 -0
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/tests/test_dependencies.py +0 -0
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/tests/test_rp.py +0 -0
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/tests/test_security_cnf_bound_as_bearer.py +0 -0
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/tests/test_security_error_oracle.py +0 -0
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/tests/test_security_id_token_substitution.py +0 -0
- {fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/tests/test_token_manager.py +0 -0
|
@@ -135,6 +135,11 @@ dmypy.json
|
|
|
135
135
|
# Allow test fixture env files (no secrets — local Docker only)
|
|
136
136
|
!.env.node-oidc
|
|
137
137
|
!.env.keycloak
|
|
138
|
+
!.env.identityserver
|
|
139
|
+
|
|
140
|
+
# Token characterization reports — generated by the integration suites,
|
|
141
|
+
# uploaded as CI artifacts, never committed.
|
|
142
|
+
token_characterization_*.md
|
|
138
143
|
*.crt
|
|
139
144
|
*.key
|
|
140
145
|
*.pfx
|
|
@@ -158,6 +163,12 @@ conformance/results/hosted/rp-logs/
|
|
|
158
163
|
.ralph/
|
|
159
164
|
PROMPT.md
|
|
160
165
|
|
|
166
|
+
# Load-report artifacts — written by the load-capacity / load-soak / load-smoke
|
|
167
|
+
# runs, uploaded as CI artifacts, never committed.
|
|
168
|
+
capacity-report.txt
|
|
169
|
+
soak-report.txt
|
|
170
|
+
load-smoke-report.txt
|
|
171
|
+
|
|
161
172
|
# Terraform
|
|
162
173
|
infra/**/.terraform/
|
|
163
174
|
infra/**/*.tfstate*
|
|
@@ -170,3 +181,6 @@ infra/**/expired_token.txt
|
|
|
170
181
|
mutants/
|
|
171
182
|
mutmut-stats.json
|
|
172
183
|
.mutmut-cache
|
|
184
|
+
|
|
185
|
+
# moon build cache — never commit (tracked cache poisons affected-detection)
|
|
186
|
+
.moon/cache/
|
|
@@ -8,13 +8,21 @@ inserted below the marker.
|
|
|
8
8
|
|
|
9
9
|
<!-- psr: next release -->
|
|
10
10
|
|
|
11
|
+
## v0.2.2 (2026-08-22)
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
- **fastapi**: Return 503 (not 401) when the OIDC provider is unreachable
|
|
16
|
+
([`26f425d`](https://github.com/jamescrowley321/identity-model/commit/26f425d61ea5e21a365d9e71fbdac0292224d5b5))
|
|
17
|
+
|
|
18
|
+
|
|
11
19
|
## v0.2.1 (2026-08-10)
|
|
12
20
|
|
|
13
21
|
### Bug Fixes
|
|
14
22
|
|
|
15
23
|
- **fastapi**: Uniform 401 body across validation failures (F-18, CWE-209)
|
|
16
|
-
([#508](https://github.com/jamescrowley321/
|
|
17
|
-
[`d4e8aed`](https://github.com/jamescrowley321/
|
|
24
|
+
([#508](https://github.com/jamescrowley321/identity-model/pull/508),
|
|
25
|
+
[`d4e8aed`](https://github.com/jamescrowley321/identity-model/commit/d4e8aedcd9e2622aab9139ae5f6b09e2e2c1eaf0))
|
|
18
26
|
|
|
19
27
|
|
|
20
28
|
## v0.2.0 (2026-08-09)
|
|
@@ -22,7 +30,7 @@ inserted below the marker.
|
|
|
22
30
|
### Features
|
|
23
31
|
|
|
24
32
|
- **fastapi**: Opt-in access-token-marker enforcement (F-07)
|
|
25
|
-
([`4e0eb10`](https://github.com/jamescrowley321/
|
|
33
|
+
([`4e0eb10`](https://github.com/jamescrowley321/identity-model/commit/4e0eb10aea9c6451614b5b6cfaa4b2c410f3496b))
|
|
26
34
|
|
|
27
35
|
|
|
28
36
|
## 0.1.0 (2026-07-08)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: fastapi-identity-model
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: FastAPI OIDC/OAuth2 middleware and relying-party router, built on py-identity-model
|
|
5
|
-
Project-URL: Homepage, https://github.com/jamescrowley321/
|
|
6
|
-
Project-URL: Repository, https://github.com/jamescrowley321/
|
|
7
|
-
Project-URL: Issues, https://github.com/jamescrowley321/
|
|
5
|
+
Project-URL: Homepage, https://github.com/jamescrowley321/identity-model
|
|
6
|
+
Project-URL: Repository, https://github.com/jamescrowley321/identity-model
|
|
7
|
+
Project-URL: Issues, https://github.com/jamescrowley321/identity-model/issues
|
|
8
8
|
Author-email: jamescrowley321 <jamescrowley151@gmail.com>
|
|
9
9
|
License-Expression: Apache-2.0
|
|
10
10
|
Keywords: authentication,fastapi,jwt,middleware,oauth2,oidc,openid-connect
|
{fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/fastapi_identity_model/middleware.py
RENAMED
|
@@ -56,6 +56,27 @@ _DEFAULT_ACCESS_TOKEN_MARKER_CLAIMS = ("scope", "scp")
|
|
|
56
56
|
# logged server-side for operators, never returned to the caller.
|
|
57
57
|
_GENERIC_401_DETAIL = "Invalid or unauthorized token"
|
|
58
58
|
|
|
59
|
+
# Stable message prefixes the core library uses for discovery/JWKS *fetch*
|
|
60
|
+
# failures (py_identity_model.core.error_handlers). The core wraps a failed
|
|
61
|
+
# fetch in an unsuccessful response, so validate_token surfaces it as a
|
|
62
|
+
# TokenValidationException — NOT a NetworkException — which would otherwise be
|
|
63
|
+
# reported as a 401 "invalid token" for what is really a transient provider
|
|
64
|
+
# outage. We match these prefixes to map such failures to 503 instead, so
|
|
65
|
+
# callers back off and 5xx monitoring fires. This is a pragmatic bridge: the
|
|
66
|
+
# clean fix is in py-identity-model (raise a NetworkException subtype from
|
|
67
|
+
# validate_disco_response / validate_jwks_response), after which the
|
|
68
|
+
# `except NetworkException` branch below handles it directly.
|
|
69
|
+
_UPSTREAM_FETCH_FAILURE_PREFIXES = (
|
|
70
|
+
"Network error during discovery",
|
|
71
|
+
"Network error during JWKS",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _is_upstream_fetch_failure(exc: Exception) -> bool:
|
|
76
|
+
"""Whether a library exception is a transient upstream fetch failure."""
|
|
77
|
+
message = str(exc)
|
|
78
|
+
return any(message.startswith(p) for p in _UPSTREAM_FETCH_FAILURE_PREFIXES)
|
|
79
|
+
|
|
59
80
|
|
|
60
81
|
class TokenValidationMiddleware(BaseHTTPMiddleware):
|
|
61
82
|
"""
|
|
@@ -213,16 +234,21 @@ class TokenValidationMiddleware(BaseHTTPMiddleware):
|
|
|
213
234
|
request.state.claims = claims
|
|
214
235
|
request.state.token = token
|
|
215
236
|
return None
|
|
216
|
-
except NetworkException:
|
|
217
|
-
# Discovery/JWKS/network fetch failure is a transient server fault,
|
|
218
|
-
# not an authentication decision — surface 5xx so callers retry
|
|
219
|
-
# instead of treating a provider outage as a bad token.
|
|
220
|
-
logger.exception("Network error during token validation")
|
|
221
|
-
return JSONResponse(
|
|
222
|
-
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
223
|
-
content={"detail": "Authentication temporarily unavailable"},
|
|
224
|
-
)
|
|
225
237
|
except PyIdentityModelException as e:
|
|
238
|
+
# A provider outage is a transient server fault, not an auth
|
|
239
|
+
# decision: surface 503 so callers retry instead of treating it as a
|
|
240
|
+
# bad token. This covers both a real NetworkException (a
|
|
241
|
+
# PyIdentityModelException subclass — the clean path if the core
|
|
242
|
+
# raises one) and a discovery/JWKS fetch failure the core currently
|
|
243
|
+
# surfaces as a TokenValidationException (_UPSTREAM_FETCH_FAILURE_
|
|
244
|
+
# PREFIXES). Checked here rather than a separate `except
|
|
245
|
+
# NetworkException` block so both share one 503 path.
|
|
246
|
+
if isinstance(e, NetworkException) or _is_upstream_fetch_failure(e):
|
|
247
|
+
logger.exception("Upstream fetch failure during token validation")
|
|
248
|
+
return JSONResponse(
|
|
249
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
250
|
+
content={"detail": "Authentication temporarily unavailable"},
|
|
251
|
+
)
|
|
226
252
|
# Uniform 401 body — do NOT echo the stage-specific cause
|
|
227
253
|
# (signature/audience/expiry), which would form a CWE-209 oracle
|
|
228
254
|
# (F-18). Log the real cause server-side for operators.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fastapi-identity-model"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "FastAPI OIDC/OAuth2 middleware and relying-party router, built on py-identity-model"
|
|
5
5
|
authors = [{ name = "jamescrowley321", email = "jamescrowley151@gmail.com" }]
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -42,9 +42,9 @@ dependencies = [
|
|
|
42
42
|
server = ["uvicorn[standard]>=0.23.0"]
|
|
43
43
|
|
|
44
44
|
[project.urls]
|
|
45
|
-
Homepage = "https://github.com/jamescrowley321/
|
|
46
|
-
Repository = "https://github.com/jamescrowley321/
|
|
47
|
-
Issues = "https://github.com/jamescrowley321/
|
|
45
|
+
Homepage = "https://github.com/jamescrowley321/identity-model"
|
|
46
|
+
Repository = "https://github.com/jamescrowley321/identity-model"
|
|
47
|
+
Issues = "https://github.com/jamescrowley321/identity-model/issues"
|
|
48
48
|
|
|
49
49
|
[tool.uv.sources]
|
|
50
50
|
py-identity-model = { workspace = true }
|
|
@@ -57,12 +57,13 @@ requires = ["hatchling"]
|
|
|
57
57
|
build-backend = "hatchling.build"
|
|
58
58
|
|
|
59
59
|
# ── Release automation (python-semantic-release) ──────────────────────
|
|
60
|
-
# Invoked from
|
|
61
|
-
# semantic-release -c packages/fastapi-identity-model/pyproject.toml version
|
|
60
|
+
# Invoked from py/ (CONS-2.1) via
|
|
61
|
+
# cd py && semantic-release -c packages/fastapi-identity-model/pyproject.toml version
|
|
62
62
|
# (release.yml, after the root release job) — all paths below are
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
63
|
+
# py/-relative (semantic-release resolves commit_parser + version_toml relative
|
|
64
|
+
# to the CWD, not to this config file's directory). Only commits scoped
|
|
65
|
+
# "(fastapi)" drive this version; every other commit belongs to the root
|
|
66
|
+
# py-identity-model pipeline. See tools/release_parsers.py for the scope routing.
|
|
66
67
|
[tool.semantic_release]
|
|
67
68
|
commit_parser = "tools/release_parsers.py:FastapiCommitParser"
|
|
68
69
|
tag_format = "fastapi-identity-model-v{version}"
|
|
@@ -86,13 +86,43 @@ async def test_unexpected_error_returns_500(monkeypatch):
|
|
|
86
86
|
assert resp.json()["detail"] == "Internal server error during authentication"
|
|
87
87
|
|
|
88
88
|
|
|
89
|
-
async def
|
|
89
|
+
async def test_network_exception_returns_503(monkeypatch):
|
|
90
|
+
# The `except NetworkException` branch: if the core raises a NetworkException
|
|
91
|
+
# subtype, a provider outage is a 503.
|
|
90
92
|
validate = AsyncMock(side_effect=DiscoveryException("provider unreachable"))
|
|
91
93
|
async with _client(_app(monkeypatch, validate)) as client:
|
|
92
94
|
resp = await client.get("/me", headers={"Authorization": "Bearer x"})
|
|
93
95
|
assert resp.status_code == 503
|
|
94
96
|
|
|
95
97
|
|
|
98
|
+
async def test_upstream_fetch_failure_returns_503(monkeypatch):
|
|
99
|
+
# The real path: a discovery/JWKS fetch failure surfaces from the core as a
|
|
100
|
+
# TokenValidationException carrying the "Network error during ..." message
|
|
101
|
+
# (NOT a NetworkException), so the middleware must still map it to 503 — a
|
|
102
|
+
# transient provider outage, not an "invalid token". (The prior test mocked
|
|
103
|
+
# DiscoveryException, which the real disco/JWKS path never raises, so this
|
|
104
|
+
# 401->503 gap went uncaught.)
|
|
105
|
+
validate = AsyncMock(
|
|
106
|
+
side_effect=TokenValidationException(
|
|
107
|
+
"Network error during discovery document request: connection refused"
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
async with _client(_app(monkeypatch, validate)) as client:
|
|
111
|
+
resp = await client.get("/me", headers={"Authorization": "Bearer x"})
|
|
112
|
+
assert resp.status_code == 503
|
|
113
|
+
assert resp.json()["detail"] == "Authentication temporarily unavailable"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def test_generic_validation_failure_still_returns_401(monkeypatch):
|
|
117
|
+
# The 503 mapping must be narrow: a genuinely invalid token (a non-network
|
|
118
|
+
# TokenValidationException) stays a 401.
|
|
119
|
+
validate = AsyncMock(side_effect=TokenValidationException("Invalid signature"))
|
|
120
|
+
async with _client(_app(monkeypatch, validate)) as client:
|
|
121
|
+
resp = await client.get("/me", headers={"Authorization": "Bearer x"})
|
|
122
|
+
assert resp.status_code == 401
|
|
123
|
+
assert resp.json()["detail"] == mw._GENERIC_401_DETAIL
|
|
124
|
+
|
|
125
|
+
|
|
96
126
|
async def test_malformed_token_returns_401_not_500(monkeypatch):
|
|
97
127
|
# A non-JWT ("invalid-token-123") makes the library's header parsing raise
|
|
98
128
|
# a raw pyjwt DecodeError before validation wraps it; that is a client
|
|
File without changes
|
{fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/fastapi_identity_model/__init__.py
RENAMED
|
File without changes
|
{fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/fastapi_identity_model/config.py
RENAMED
|
File without changes
|
{fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/fastapi_identity_model/dependencies.py
RENAMED
|
File without changes
|
{fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/fastapi_identity_model/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_identity_model-0.2.1 → fastapi_identity_model-0.2.2}/tests/test_security_error_oracle.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|