fastapi-identity-model 0.2.2__tar.gz → 0.2.3__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.2 → fastapi_identity_model-0.2.3}/.gitignore +3 -0
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/CHANGELOG.md +15 -0
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/PKG-INFO +1 -1
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/fastapi_identity_model/rp.py +22 -9
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/pyproject.toml +1 -1
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/tests/test_rp.py +84 -15
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/README.md +0 -0
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/fastapi_identity_model/__init__.py +0 -0
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/fastapi_identity_model/config.py +0 -0
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/fastapi_identity_model/dependencies.py +0 -0
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/fastapi_identity_model/middleware.py +0 -0
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/fastapi_identity_model/py.typed +0 -0
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/fastapi_identity_model/token_manager.py +0 -0
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/tests/test_config.py +0 -0
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/tests/test_dependencies.py +0 -0
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/tests/test_middleware.py +0 -0
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/tests/test_security_cnf_bound_as_bearer.py +0 -0
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/tests/test_security_error_oracle.py +0 -0
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/tests/test_security_id_token_substitution.py +0 -0
- {fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/tests/test_token_manager.py +0 -0
|
@@ -8,6 +8,21 @@ inserted below the marker.
|
|
|
8
8
|
|
|
9
9
|
<!-- psr: next release -->
|
|
10
10
|
|
|
11
|
+
## v0.2.3 (2026-09-04)
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
- **fastapi**: Don't reflect provider/library error internals from the RP callback
|
|
16
|
+
([#622](https://github.com/jamescrowley321/identity-model/pull/622),
|
|
17
|
+
[`2480587`](https://github.com/jamescrowley321/identity-model/commit/2480587a1ef28837ce9b36f928dcd8cef26e07cd))
|
|
18
|
+
|
|
19
|
+
### Testing
|
|
20
|
+
|
|
21
|
+
- **fastapi**: Cover the two genericized error branches
|
|
22
|
+
([#622](https://github.com/jamescrowley321/identity-model/pull/622),
|
|
23
|
+
[`2480587`](https://github.com/jamescrowley321/identity-model/commit/2480587a1ef28837ce9b36f928dcd8cef26e07cd))
|
|
24
|
+
|
|
25
|
+
|
|
11
26
|
## v0.2.2 (2026-08-22)
|
|
12
27
|
|
|
13
28
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fastapi-identity-model
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: FastAPI OIDC/OAuth2 middleware and relying-party router, built on py-identity-model
|
|
5
5
|
Project-URL: Homepage, https://github.com/jamescrowley321/identity-model
|
|
6
6
|
Project-URL: Repository, https://github.com/jamescrowley321/identity-model
|
|
@@ -103,9 +103,13 @@ async def _discover(settings: OIDCSettings) -> DiscoveryDocumentResponse:
|
|
|
103
103
|
DiscoveryDocumentRequest(address=settings.discovery_url),
|
|
104
104
|
)
|
|
105
105
|
if not disco.is_successful:
|
|
106
|
+
# Log the provider's specific error for operators; return a generic
|
|
107
|
+
# detail so the callback endpoint cannot be probed for upstream
|
|
108
|
+
# internals (mirrors the resource-server middleware's F-18 posture).
|
|
109
|
+
logger.warning("OIDC discovery failed: %s", disco.error)
|
|
106
110
|
raise HTTPException(
|
|
107
111
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
|
108
|
-
detail=
|
|
112
|
+
detail="Identity provider discovery failed",
|
|
109
113
|
)
|
|
110
114
|
# OIDC Discovery 1.0 §4.3: the document's issuer MUST match the URL the
|
|
111
115
|
# document was retrieved from (issuer mix-up defense). The library only
|
|
@@ -114,12 +118,14 @@ async def _discover(settings: OIDCSettings) -> DiscoveryDocumentResponse:
|
|
|
114
118
|
# document at "https://op/.well-known/openid-configuration".
|
|
115
119
|
expected = _expected_issuer(settings.discovery_url)
|
|
116
120
|
if (disco.issuer or "").rstrip("/") != expected:
|
|
121
|
+
logger.warning(
|
|
122
|
+
"Discovery document issuer mismatch: expected %r, got %r",
|
|
123
|
+
expected,
|
|
124
|
+
disco.issuer,
|
|
125
|
+
)
|
|
117
126
|
raise HTTPException(
|
|
118
127
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
|
119
|
-
detail=
|
|
120
|
-
f"Discovery document issuer mismatch: expected '{expected}', "
|
|
121
|
-
f"got '{disco.issuer}'"
|
|
122
|
-
),
|
|
128
|
+
detail="Identity provider configuration error",
|
|
123
129
|
)
|
|
124
130
|
return disco
|
|
125
131
|
|
|
@@ -181,9 +187,10 @@ async def _exchange_code(
|
|
|
181
187
|
),
|
|
182
188
|
)
|
|
183
189
|
if not tok.is_successful or not tok.token:
|
|
190
|
+
logger.warning("Token exchange failed: %s", tok.error)
|
|
184
191
|
raise HTTPException(
|
|
185
192
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
186
|
-
detail=
|
|
193
|
+
detail="Authorization code exchange failed",
|
|
187
194
|
)
|
|
188
195
|
return tok.token
|
|
189
196
|
|
|
@@ -210,9 +217,10 @@ async def _validate_id_token(
|
|
|
210
217
|
disco_doc_address=settings.discovery_url,
|
|
211
218
|
)
|
|
212
219
|
except PyIdentityModelException as exc:
|
|
220
|
+
logger.info("ID token validation failed: %s", exc)
|
|
213
221
|
raise HTTPException(
|
|
214
222
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
215
|
-
detail=
|
|
223
|
+
detail="ID token validation failed",
|
|
216
224
|
) from exc
|
|
217
225
|
|
|
218
226
|
# Nonce binding is the RP's responsibility — the library does not check it.
|
|
@@ -278,14 +286,19 @@ async def _callback( # noqa: PLR0913 # request-scoped args + the router's buil
|
|
|
278
286
|
try:
|
|
279
287
|
cb = parse_authorize_callback_response(callback_url)
|
|
280
288
|
except PyIdentityModelException as exc:
|
|
289
|
+
logger.info("Malformed authorization callback: %s", exc)
|
|
281
290
|
raise HTTPException(
|
|
282
291
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
283
|
-
detail=
|
|
292
|
+
detail="Malformed authorization response",
|
|
284
293
|
) from exc
|
|
285
294
|
if not cb.is_successful:
|
|
295
|
+
# ``cb.error`` is the provider's OAuth 2.0 ``error`` param, but it can
|
|
296
|
+
# carry an attacker-influenced ``error_description``; log it, return a
|
|
297
|
+
# generic detail rather than reflecting it to the browser.
|
|
298
|
+
logger.info("Authorization error from provider: %s", cb.error)
|
|
286
299
|
raise HTTPException(
|
|
287
300
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
288
|
-
detail=
|
|
301
|
+
detail="Authorization request failed",
|
|
289
302
|
)
|
|
290
303
|
if not validate_authorize_callback_state(cb, flow["state"]).is_valid:
|
|
291
304
|
raise HTTPException(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fastapi-identity-model"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3"
|
|
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"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from types import SimpleNamespace
|
|
2
3
|
from unittest.mock import AsyncMock
|
|
3
4
|
from urllib.parse import parse_qs, urlparse
|
|
@@ -8,7 +9,7 @@ import pytest
|
|
|
8
9
|
from starlette.middleware.sessions import SessionMiddleware
|
|
9
10
|
|
|
10
11
|
from fastapi_identity_model import OIDCSettings, build_oidc_router, rp
|
|
11
|
-
from py_identity_model import TokenValidationException
|
|
12
|
+
from py_identity_model import AuthorizeCallbackException, TokenValidationException
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
pytestmark = pytest.mark.unit
|
|
@@ -183,15 +184,20 @@ async def test_callback_missing_code(monkeypatch):
|
|
|
183
184
|
assert "missing code" in resp.json()["detail"]
|
|
184
185
|
|
|
185
186
|
|
|
186
|
-
async def test_login_discovery_failure(monkeypatch):
|
|
187
|
+
async def test_login_discovery_failure(monkeypatch, caplog):
|
|
187
188
|
_patch(monkeypatch, disco=SimpleNamespace(is_successful=False, error="down"))
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
with caplog.at_level(logging.WARNING, logger="fastapi_identity_model"):
|
|
190
|
+
async with _client(_app()) as client:
|
|
191
|
+
resp = await client.get("/auth/login")
|
|
190
192
|
assert resp.status_code == 502
|
|
191
|
-
|
|
193
|
+
# Generic client detail — the provider's specific error must not be echoed
|
|
194
|
+
# to the browser (#601), but IS logged server-side for operators.
|
|
195
|
+
assert resp.json()["detail"] == "Identity provider discovery failed"
|
|
196
|
+
assert "down" not in resp.json()["detail"]
|
|
197
|
+
assert "down" in caplog.text
|
|
192
198
|
|
|
193
199
|
|
|
194
|
-
async def test_login_rejects_discovery_issuer_mismatch(monkeypatch):
|
|
200
|
+
async def test_login_rejects_discovery_issuer_mismatch(monkeypatch, caplog):
|
|
195
201
|
# OIDC Discovery 1.0 §4.3: a document whose issuer does not match the
|
|
196
202
|
# URL it was retrieved from must be rejected (issuer mix-up defense).
|
|
197
203
|
mismatched = SimpleNamespace(
|
|
@@ -203,10 +209,15 @@ async def test_login_rejects_discovery_issuer_mismatch(monkeypatch):
|
|
|
203
209
|
issuer="https://op/INVALID",
|
|
204
210
|
)
|
|
205
211
|
_patch(monkeypatch, disco=mismatched)
|
|
206
|
-
|
|
207
|
-
|
|
212
|
+
with caplog.at_level(logging.WARNING, logger="fastapi_identity_model"):
|
|
213
|
+
async with _client(_app()) as client:
|
|
214
|
+
resp = await client.get("/auth/login")
|
|
208
215
|
assert resp.status_code == 502
|
|
209
|
-
|
|
216
|
+
# The mismatched issuer value is not reflected to the client (#601)...
|
|
217
|
+
assert resp.json()["detail"] == "Identity provider configuration error"
|
|
218
|
+
assert "INVALID" not in resp.json()["detail"]
|
|
219
|
+
# ...but the expected/got pair is logged for operators.
|
|
220
|
+
assert "INVALID" in caplog.text
|
|
210
221
|
|
|
211
222
|
|
|
212
223
|
async def test_fetch_userinfo_disabled_skips_userinfo(monkeypatch):
|
|
@@ -224,26 +235,84 @@ async def test_fetch_userinfo_disabled_skips_userinfo(monkeypatch):
|
|
|
224
235
|
rp.get_userinfo.assert_not_called()
|
|
225
236
|
|
|
226
237
|
|
|
227
|
-
async def test_callback_token_exchange_failure(monkeypatch):
|
|
238
|
+
async def test_callback_token_exchange_failure(monkeypatch, caplog):
|
|
228
239
|
_patch(monkeypatch)
|
|
229
240
|
async with _client(_app()) as client:
|
|
230
241
|
state, _ = await _login(client)
|
|
231
242
|
rp.request_authorization_code_token.return_value = SimpleNamespace(
|
|
232
243
|
is_successful=False, error="invalid_grant", token=None
|
|
233
244
|
)
|
|
234
|
-
|
|
245
|
+
with caplog.at_level(logging.WARNING, logger="fastapi_identity_model"):
|
|
246
|
+
resp = await client.get(f"/auth/callback?code=abc&state={state}")
|
|
235
247
|
assert resp.status_code == 400
|
|
236
|
-
|
|
248
|
+
# The provider's grant error is logged, not reflected to the client (#601).
|
|
249
|
+
assert resp.json()["detail"] == "Authorization code exchange failed"
|
|
250
|
+
assert "invalid_grant" not in resp.json()["detail"]
|
|
251
|
+
assert "invalid_grant" in caplog.text
|
|
237
252
|
|
|
238
253
|
|
|
239
|
-
async def test_callback_invalid_id_token(monkeypatch):
|
|
254
|
+
async def test_callback_invalid_id_token(monkeypatch, caplog):
|
|
240
255
|
_patch(monkeypatch)
|
|
241
256
|
async with _client(_app()) as client:
|
|
242
257
|
state, _ = await _login(client)
|
|
243
258
|
rp.validate_token.side_effect = TokenValidationException("bad iss")
|
|
244
|
-
|
|
259
|
+
with caplog.at_level(logging.INFO, logger="fastapi_identity_model"):
|
|
260
|
+
resp = await client.get(f"/auth/callback?code=abc&state={state}")
|
|
245
261
|
assert resp.status_code == 401
|
|
246
|
-
|
|
262
|
+
# Generic detail — the specific validation cause ("bad iss") is logged, not
|
|
263
|
+
# returned, so the callback can't be used as a validation-stage oracle (#601).
|
|
264
|
+
assert resp.json()["detail"] == "ID token validation failed"
|
|
265
|
+
assert "bad iss" not in resp.json()["detail"]
|
|
266
|
+
assert "bad iss" in caplog.text
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
async def test_callback_malformed_response_detail_is_generic(monkeypatch, caplog):
|
|
270
|
+
# A parse failure must return a generic detail: the library's exception
|
|
271
|
+
# text (which can carry attacker-influenced callback contents) is logged
|
|
272
|
+
# server-side, never reflected to the browser (#601).
|
|
273
|
+
secret = "parse-cause-4c2e9a-do-not-leak"
|
|
274
|
+
|
|
275
|
+
def _raise(_url):
|
|
276
|
+
raise AuthorizeCallbackException(secret)
|
|
277
|
+
|
|
278
|
+
_patch(monkeypatch)
|
|
279
|
+
monkeypatch.setattr(rp, "parse_authorize_callback_response", _raise)
|
|
280
|
+
async with _client(_app()) as client:
|
|
281
|
+
state, _ = await _login(client)
|
|
282
|
+
with caplog.at_level(logging.INFO, logger="fastapi_identity_model"):
|
|
283
|
+
resp = await client.get(f"/auth/callback?code=abc&state={state}")
|
|
284
|
+
assert resp.status_code == 400
|
|
285
|
+
# Generic detail — pre-#622 this reflected f"...: {exc}"; the parser's
|
|
286
|
+
# exception text must not appear anywhere in the response body.
|
|
287
|
+
assert resp.json()["detail"] == "Malformed authorization response"
|
|
288
|
+
assert secret not in resp.text
|
|
289
|
+
# ...but the real cause IS logged for operators.
|
|
290
|
+
assert secret in caplog.text
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
async def test_callback_provider_error_detail_is_generic(monkeypatch, caplog):
|
|
294
|
+
# A provider error response (?error=...) must return a generic detail:
|
|
295
|
+
# neither the provider's error code nor the attacker-controllable
|
|
296
|
+
# error_description is reflected to the browser (#601).
|
|
297
|
+
secret = "err-desc-secret-8b7c1d-do-not-leak"
|
|
298
|
+
_patch(monkeypatch)
|
|
299
|
+
async with _client(_app()) as client:
|
|
300
|
+
state, _ = await _login(client)
|
|
301
|
+
with caplog.at_level(logging.INFO, logger="fastapi_identity_model"):
|
|
302
|
+
resp = await client.get(
|
|
303
|
+
f"/auth/callback?state={state}"
|
|
304
|
+
f"&error=access_denied&error_description={secret}"
|
|
305
|
+
)
|
|
306
|
+
assert resp.status_code == 400
|
|
307
|
+
# Generic detail — pre-#622 this reflected f"Authorization error: {cb.error}".
|
|
308
|
+
assert resp.json()["detail"] == "Authorization request failed"
|
|
309
|
+
# Neither the error code nor the attacker-influenced description leaks to
|
|
310
|
+
# the client...
|
|
311
|
+
assert "access_denied" not in resp.text
|
|
312
|
+
assert secret not in resp.text
|
|
313
|
+
# ...but the error code IS logged for operators (the description is not).
|
|
314
|
+
assert "access_denied" in caplog.text
|
|
315
|
+
assert secret not in caplog.text
|
|
247
316
|
|
|
248
317
|
|
|
249
318
|
async def test_callback_nonce_mismatch(monkeypatch):
|
|
File without changes
|
{fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/fastapi_identity_model/__init__.py
RENAMED
|
File without changes
|
{fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/fastapi_identity_model/config.py
RENAMED
|
File without changes
|
{fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/fastapi_identity_model/dependencies.py
RENAMED
|
File without changes
|
{fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/fastapi_identity_model/middleware.py
RENAMED
|
File without changes
|
{fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/fastapi_identity_model/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_identity_model-0.2.2 → fastapi_identity_model-0.2.3}/tests/test_security_error_oracle.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|