oidcauthlib 3.0.22__tar.gz → 3.0.23__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.
- {oidcauthlib-3.0.22/oidcauthlib.egg-info → oidcauthlib-3.0.23}/PKG-INFO +11 -8
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/README.md +10 -7
- oidcauthlib-3.0.23/VERSION +1 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/userinfo/oidc_userinfo_manager.py +80 -22
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23/oidcauthlib.egg-info}/PKG-INFO +11 -8
- oidcauthlib-3.0.22/VERSION +0 -1
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/LICENSE +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/MANIFEST.in +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/auth_helper.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/auth_manager.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/cache/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/cache/oauth_cache.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/cache/oauth_memory_cache.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/cache/oauth_mongo_cache.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/config/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/config/auth_config.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/config/auth_config_reader.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/dcr/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/dcr/dcr_client.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/dcr/dcr_manager.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/dcr/dcr_registration.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/exceptions/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/exceptions/authorization_bearer_token_expired_exception.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/exceptions/authorization_bearer_token_invalid_exception.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/exceptions/authorization_bearer_token_missing_exception.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/exceptions/authorization_needed_exception.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/fastapi_auth_manager.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/middleware/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/middleware/request_scope_middleware.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/middleware/token_reader_middleware.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/models/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/models/auth.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/models/base_db_model.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/models/cache_item.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/models/client_key_set.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/models/token.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/repository/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/repository/base_repository.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/repository/memory/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/repository/memory/memory_repository.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/repository/mongo/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/repository/mongo/mongo_repository.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/repository/repository_factory.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/routers/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/routers/auth_router.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/token_reader.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/userinfo/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/well_known_configuration/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/well_known_configuration/auth_server_metadata.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/well_known_configuration/auth_server_metadata_discovery.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/well_known_configuration/well_known_configuration_cache.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/well_known_configuration/well_known_configuration_cache_result.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/well_known_configuration/well_known_configuration_manager.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/cache_gate/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/cache_gate/advisory_lock.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/cache_gate/version_gate.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/container/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/container/oidc_authlib_container_factory.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/open_telemetry/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/open_telemetry/attribute_names.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/open_telemetry/configurator.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/open_telemetry/filtering_sampler.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/open_telemetry/filtering_span_exporter.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/open_telemetry/filtering_span_processor.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/open_telemetry/otel_setup.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/open_telemetry/span_names.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/py.typed +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/storage/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/storage/cache_to_collection_mapper.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/storage/memory_storage_factory.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/storage/mongo_gridfs_db.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/storage/mongo_gridfs_exception.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/storage/mongo_storage_factory.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/storage/storage_factory.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/storage/storage_factory_creator.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/utilities/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/utilities/cached.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/utilities/environment/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/utilities/environment/abstract_environment_variables.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/utilities/environment/oidc_environment_variables.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/utilities/logger/__init__.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/utilities/logger/log_levels.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/utilities/logger/logging_response.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/utilities/logger/logging_transport.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/utilities/mongo_url_utils.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/utilities/url_validator.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib.egg-info/SOURCES.txt +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib.egg-info/dependency_links.txt +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib.egg-info/requires.txt +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib.egg-info/top_level.txt +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/pyproject.toml +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/setup.cfg +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/tests/test_mongo_url_utils.py +0 -0
- {oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/tests/test_simple.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oidcauthlib
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.23
|
|
4
4
|
Summary: oidcauthlib
|
|
5
5
|
Author-email: Imran Qureshi <imran.qureshi@bwell.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -55,14 +55,17 @@ scopes = await manager.get_scopes_from_user_info_async(access_token=token)
|
|
|
55
55
|
`scope_claims`, then `group_claims` (each value taken whole). Commas separate like spaces, results are
|
|
56
56
|
de-duplicated, and `scope_remove_prefixes` are stripped (first match wins). Nothing beyond `scope`/`scp`
|
|
57
57
|
is read unless configured.
|
|
58
|
-
- **Provider** is matched to the token's `iss
|
|
59
|
-
|
|
60
|
-
never sent to another provider.
|
|
58
|
+
- **Provider** is matched to the token's `iss`: an exact `issuer`, else a `well_known_uri` on the same scheme,
|
|
59
|
+
host and port whose path segments start with the issuer's. Only a token with no `iss` at all is matched by
|
|
60
|
+
its client id. A token matching no provider raises `ValueError`; it is never sent to another provider.
|
|
61
|
+
- **Subject check:** the userinfo response's `sub` must equal the token's `sub` (OIDC Core 5.3.2); otherwise
|
|
62
|
+
the lookup raises `ValueError` and nothing is cached.
|
|
61
63
|
- **Cache** key is `oidc-userinfo-scopes:<issuer>:<client_id>:<sub>|v1` with each segment percent-encoded
|
|
62
64
|
(so values containing `:` cannot collide, and two providers never share an entry); only the
|
|
63
65
|
userinfo-derived scopes are stored, never the userinfo response. The token's own scopes are re-derived on every call. A token without
|
|
64
|
-
`sub
|
|
66
|
+
`sub` or without an issuer, or an empty scope list, is not cached. Changes to a user's entitlements are visible after at most
|
|
65
67
|
`cache_ttl_seconds`.
|
|
66
|
-
- **Failures:**
|
|
67
|
-
entry
|
|
68
|
-
or
|
|
68
|
+
- **Failures:** the cache never fails a lookup. A write failure is logged and swallowed; a read failure or a
|
|
69
|
+
malformed entry is logged and treated as a miss, and the refetched scopes overwrite the bad entry. A
|
|
70
|
+
userinfo HTTP error, a missing provider or a mismatched subject is raised, so each service decides whether
|
|
71
|
+
to fail closed or degrade.
|
|
@@ -27,14 +27,17 @@ scopes = await manager.get_scopes_from_user_info_async(access_token=token)
|
|
|
27
27
|
`scope_claims`, then `group_claims` (each value taken whole). Commas separate like spaces, results are
|
|
28
28
|
de-duplicated, and `scope_remove_prefixes` are stripped (first match wins). Nothing beyond `scope`/`scp`
|
|
29
29
|
is read unless configured.
|
|
30
|
-
- **Provider** is matched to the token's `iss
|
|
31
|
-
|
|
32
|
-
never sent to another provider.
|
|
30
|
+
- **Provider** is matched to the token's `iss`: an exact `issuer`, else a `well_known_uri` on the same scheme,
|
|
31
|
+
host and port whose path segments start with the issuer's. Only a token with no `iss` at all is matched by
|
|
32
|
+
its client id. A token matching no provider raises `ValueError`; it is never sent to another provider.
|
|
33
|
+
- **Subject check:** the userinfo response's `sub` must equal the token's `sub` (OIDC Core 5.3.2); otherwise
|
|
34
|
+
the lookup raises `ValueError` and nothing is cached.
|
|
33
35
|
- **Cache** key is `oidc-userinfo-scopes:<issuer>:<client_id>:<sub>|v1` with each segment percent-encoded
|
|
34
36
|
(so values containing `:` cannot collide, and two providers never share an entry); only the
|
|
35
37
|
userinfo-derived scopes are stored, never the userinfo response. The token's own scopes are re-derived on every call. A token without
|
|
36
|
-
`sub
|
|
38
|
+
`sub` or without an issuer, or an empty scope list, is not cached. Changes to a user's entitlements are visible after at most
|
|
37
39
|
`cache_ttl_seconds`.
|
|
38
|
-
- **Failures:**
|
|
39
|
-
entry
|
|
40
|
-
or
|
|
40
|
+
- **Failures:** the cache never fails a lookup. A write failure is logged and swallowed; a read failure or a
|
|
41
|
+
malformed entry is logged and treated as a miss, and the refetched scopes overwrite the bad entry. A
|
|
42
|
+
userinfo HTTP error, a missing provider or a mismatched subject is raised, so each service decides whether
|
|
43
|
+
to fail closed or degrade.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.0.23
|
{oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/userinfo/oidc_userinfo_manager.py
RENAMED
|
@@ -2,7 +2,7 @@ import hashlib
|
|
|
2
2
|
import logging
|
|
3
3
|
from collections.abc import Sequence
|
|
4
4
|
from typing import Any
|
|
5
|
-
from urllib.parse import quote
|
|
5
|
+
from urllib.parse import SplitResult, quote, urlsplit
|
|
6
6
|
|
|
7
7
|
import httpx
|
|
8
8
|
from key_value.aio.protocols.key_value import AsyncKeyValueProtocol
|
|
@@ -21,6 +21,8 @@ from oidcauthlib.utilities.logger.log_levels import SRC_LOG_LEVELS
|
|
|
21
21
|
logger = logging.getLogger(__name__)
|
|
22
22
|
logger.setLevel(SRC_LOG_LEVELS["AUTH"])
|
|
23
23
|
|
|
24
|
+
_DEFAULT_PORTS = {"http": 80, "https": 443}
|
|
25
|
+
|
|
24
26
|
|
|
25
27
|
class OidcUserInfoCacheEntry(BaseModel):
|
|
26
28
|
"""Document stored for one cached scope list."""
|
|
@@ -37,9 +39,12 @@ class OidcUserInfoManager:
|
|
|
37
39
|
JWT; the scopes that authorize a caller appear only on the userinfo response.
|
|
38
40
|
|
|
39
41
|
Behavior:
|
|
40
|
-
- The provider is matched to the token's ``iss`` claim (exact ``issuer``,
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
- The provider is matched to the token's ``iss`` claim (exact ``issuer``, else a ``well_known_uri`` on the
|
|
43
|
+
same scheme, host and port whose path segments start with the issuer's). Only a token with no ``iss`` at
|
|
44
|
+
all is matched by its client id. A token matching no provider is an error: the bearer token is never
|
|
45
|
+
sent to an unrelated provider.
|
|
46
|
+
- The userinfo response's ``sub`` must equal the token's ``sub`` (OIDC Core 5.3.2), else the lookup fails
|
|
47
|
+
and nothing is cached.
|
|
43
48
|
- The userinfo endpoint is discovered through ``WellKnownConfigurationManager``.
|
|
44
49
|
- Scopes are derived the way fhir-server-ui's ``jwtParser`` does: the ``scope`` claim (plus ``scp``,
|
|
45
50
|
which Okta uses), the claims named in ``scope_claims``, and each claim named in ``group_claims``
|
|
@@ -52,16 +57,19 @@ class OidcUserInfoManager:
|
|
|
52
57
|
``oidc-userinfo-scopes:<issuer>:<client_id>:<sub>|v<CACHE_VERSION>`` (each segment percent-encoded). The token's own scopes are always
|
|
53
58
|
re-derived from the presented token, so a narrower token never inherits a broader cached set.
|
|
54
59
|
Bumping ``CACHE_VERSION`` obsoletes every existing entry.
|
|
55
|
-
- A token without ``sub`` is never cached: the key would be shared by every user of
|
|
60
|
+
- A token without ``sub`` or without an issuer is never cached: the key would be shared by every user of
|
|
61
|
+
the client, or by every provider that issues tokens without ``iss``.
|
|
56
62
|
- An empty scope list is never cached: it is also what an account with no scopes looks like, and
|
|
57
63
|
caching it would pin that result for the whole TTL.
|
|
58
|
-
-
|
|
64
|
+
- The cache is an optimization and never fails a lookup: a write failure is logged and swallowed, and a
|
|
65
|
+
read failure or an entry of the wrong shape is logged and treated as a miss (the refetched scopes
|
|
66
|
+
overwrite a bad entry).
|
|
59
67
|
|
|
60
68
|
The store and collection are supplied by the caller, so each service keeps its own database. A cached
|
|
61
69
|
scope list outlives a change to the user's entitlements by up to ``cache_ttl_seconds``.
|
|
62
70
|
|
|
63
|
-
Failures of the lookup itself (no matching provider, HTTP error,
|
|
64
|
-
|
|
71
|
+
Failures of the lookup itself (no matching provider, HTTP error, mismatched subject) are raised so the
|
|
72
|
+
caller decides whether to fail closed or degrade.
|
|
65
73
|
"""
|
|
66
74
|
|
|
67
75
|
CACHE_VERSION = 1
|
|
@@ -108,18 +116,24 @@ class OidcUserInfoManager:
|
|
|
108
116
|
def resolve_auth_config(self, *, access_token: Token) -> AuthConfig:
|
|
109
117
|
"""The configuration of the provider that issued ``access_token``.
|
|
110
118
|
|
|
119
|
+
The token's issuer decides: an exact ``issuer`` match, else a ``well_known_uri`` on the same scheme, host
|
|
120
|
+
and port whose path segments start with the issuer's. The client id is only used for a token that has
|
|
121
|
+
no issuer at all; a token whose issuer matches no provider is never routed by its client id, which
|
|
122
|
+
would send it to whichever provider owns that client id.
|
|
123
|
+
|
|
111
124
|
Raises:
|
|
112
|
-
ValueError: no configured provider matches the token
|
|
125
|
+
ValueError: no configured provider matches the token.
|
|
113
126
|
"""
|
|
114
127
|
issuer = access_token.issuer or (access_token.claims or {}).get("iss")
|
|
115
|
-
configs = self._auth_config_reader.get_auth_configs_for_all_auth_providers()
|
|
116
128
|
if issuer:
|
|
129
|
+
configs = self._auth_config_reader.get_auth_configs_for_all_auth_providers()
|
|
117
130
|
for config in configs:
|
|
118
131
|
if config.issuer and config.issuer == issuer:
|
|
119
132
|
return config
|
|
120
133
|
for config in configs:
|
|
121
134
|
if config.well_known_uri and self._is_under_issuer(uri=config.well_known_uri, issuer=issuer):
|
|
122
135
|
return config
|
|
136
|
+
raise ValueError(f"No auth provider matches token issuer {issuer!r}")
|
|
123
137
|
client_id = access_token.client_id
|
|
124
138
|
if client_id:
|
|
125
139
|
auth_provider = self._auth_config_reader.get_provider_for_client_id(client_id=client_id)
|
|
@@ -127,17 +141,38 @@ class OidcUserInfoManager:
|
|
|
127
141
|
auth_config = self._auth_config_reader.get_config_for_auth_provider(auth_provider=auth_provider)
|
|
128
142
|
if auth_config:
|
|
129
143
|
return auth_config
|
|
130
|
-
raise ValueError(f"No auth provider matches token
|
|
144
|
+
raise ValueError(f"No auth provider matches token client id {client_id!r}")
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _effective_port(*, parts: SplitResult) -> int | None:
|
|
148
|
+
if parts.port is not None:
|
|
149
|
+
return parts.port
|
|
150
|
+
return _DEFAULT_PORTS.get(parts.scheme.lower())
|
|
131
151
|
|
|
132
152
|
@staticmethod
|
|
133
153
|
def _is_under_issuer(*, uri: str, issuer: str) -> bool:
|
|
134
|
-
"""True when ``uri`` is the issuer
|
|
154
|
+
"""True when ``uri`` is on the issuer's origin and its path is the issuer's path or beneath it.
|
|
135
155
|
|
|
136
|
-
|
|
137
|
-
``https://a.example.com
|
|
156
|
+
Scheme, host (case-insensitive), port (default ports normalized) and path *segments* are compared.
|
|
157
|
+
A string prefix test would match ``https://a.example.com.au/...`` for ``https://a.example.com``,
|
|
158
|
+
``/realms/ab`` for ``/realms/a``, and every https URL for the degenerate issuer ``https://``.
|
|
138
159
|
"""
|
|
139
|
-
|
|
140
|
-
|
|
160
|
+
try:
|
|
161
|
+
issuer_parts = urlsplit(issuer)
|
|
162
|
+
uri_parts = urlsplit(uri)
|
|
163
|
+
issuer_port = OidcUserInfoManager._effective_port(parts=issuer_parts)
|
|
164
|
+
uri_port = OidcUserInfoManager._effective_port(parts=uri_parts)
|
|
165
|
+
except ValueError:
|
|
166
|
+
return False
|
|
167
|
+
if not issuer_parts.scheme or not issuer_parts.hostname or not uri_parts.hostname:
|
|
168
|
+
return False
|
|
169
|
+
if uri_parts.scheme.lower() != issuer_parts.scheme.lower():
|
|
170
|
+
return False
|
|
171
|
+
if uri_parts.hostname.lower() != issuer_parts.hostname.lower() or uri_port != issuer_port:
|
|
172
|
+
return False
|
|
173
|
+
issuer_segments = [segment for segment in issuer_parts.path.split("/") if segment]
|
|
174
|
+
uri_segments = [segment for segment in uri_parts.path.split("/") if segment]
|
|
175
|
+
return uri_segments[: len(issuer_segments)] == issuer_segments
|
|
141
176
|
|
|
142
177
|
@staticmethod
|
|
143
178
|
def _loggable(*, cache_key: str) -> str:
|
|
@@ -149,8 +184,12 @@ class OidcUserInfoManager:
|
|
|
149
184
|
|
|
150
185
|
``access_token`` must already have been verified by the caller; its claims are trusted.
|
|
151
186
|
|
|
187
|
+
Per OIDC Core 5.3.2 the response's ``sub`` must equal the token's ``sub``; a response that carries a
|
|
188
|
+
different ``sub`` describes another user and is rejected.
|
|
189
|
+
|
|
152
190
|
Raises:
|
|
153
|
-
ValueError: the token, auth config, discovery document or response is unusable
|
|
191
|
+
ValueError: the token, auth config, discovery document or response is unusable, or the response
|
|
192
|
+
is for a different subject.
|
|
154
193
|
httpx.HTTPError: the userinfo request failed.
|
|
155
194
|
"""
|
|
156
195
|
if not access_token.token:
|
|
@@ -182,6 +221,10 @@ class OidcUserInfoManager:
|
|
|
182
221
|
user_info = response.json()
|
|
183
222
|
if not isinstance(user_info, dict):
|
|
184
223
|
raise ValueError("User info response is not a valid JSON object")
|
|
224
|
+
response_subject = user_info.get("sub")
|
|
225
|
+
if response_subject is not None and access_token.subject and response_subject != access_token.subject:
|
|
226
|
+
# Neither subject is logged: they identify users.
|
|
227
|
+
raise ValueError("User info response is for a different subject than the token")
|
|
185
228
|
return user_info
|
|
186
229
|
|
|
187
230
|
async def get_scopes_from_user_info_async(self, *, access_token: Token) -> list[str]:
|
|
@@ -190,7 +233,7 @@ class OidcUserInfoManager:
|
|
|
190
233
|
The userinfo scopes come from the cache when present, else from the endpoint (then cached).
|
|
191
234
|
|
|
192
235
|
Raises:
|
|
193
|
-
ValueError:
|
|
236
|
+
ValueError: no provider matches the token, or the userinfo response is for another subject.
|
|
194
237
|
httpx.HTTPError: the userinfo request failed.
|
|
195
238
|
"""
|
|
196
239
|
tracer = trace.get_tracer(__name__)
|
|
@@ -264,18 +307,33 @@ class OidcUserInfoManager:
|
|
|
264
307
|
return f"{':'.join([cls._KEY_PREFIX, *segments])}|v{cls.CACHE_VERSION}"
|
|
265
308
|
|
|
266
309
|
async def _read_cached_scopes(self, *, cache_key: str) -> list[str] | None:
|
|
267
|
-
|
|
310
|
+
"""Cached userinfo scopes, or None for a miss.
|
|
311
|
+
|
|
312
|
+
A store failure or an entry of the wrong shape is logged and treated as a miss: the cache is an
|
|
313
|
+
optimization, so one bad entry (or a store outage) must not fail every request for that key until the
|
|
314
|
+
TTL expires. The refetched scopes then overwrite the bad entry.
|
|
315
|
+
"""
|
|
268
316
|
key_id = self._loggable(cache_key=cache_key)
|
|
317
|
+
try:
|
|
318
|
+
raw = await self._store.get(cache_key, collection=self._cache_collection)
|
|
319
|
+
except Exception:
|
|
320
|
+
logger.exception(f"Failed to read OIDC userinfo scopes from the cache for key: {key_id}")
|
|
321
|
+
return None
|
|
269
322
|
if raw is None:
|
|
270
323
|
logger.debug(f"OIDC userinfo scopes cache miss for key: {key_id}")
|
|
271
324
|
return None
|
|
272
325
|
try:
|
|
273
326
|
entry = OidcUserInfoCacheEntry.model_validate(raw)
|
|
274
|
-
except ValueError
|
|
275
|
-
|
|
327
|
+
except ValueError:
|
|
328
|
+
logger.warning(f"OIDC userinfo scopes cache entry has invalid shape for key: {key_id}; refetching")
|
|
329
|
+
return None
|
|
276
330
|
scopes = entry.value.get("scopes")
|
|
277
331
|
if not isinstance(scopes, list) or not all(isinstance(s, str) for s in scopes):
|
|
278
|
-
|
|
332
|
+
logger.warning(
|
|
333
|
+
f"OIDC userinfo scopes cache entry has invalid scopes ({type(scopes).__name__}) for key: {key_id}; "
|
|
334
|
+
"refetching"
|
|
335
|
+
)
|
|
336
|
+
return None
|
|
279
337
|
return scopes
|
|
280
338
|
|
|
281
339
|
async def _write_cached_scopes(self, *, cache_key: str, scopes: list[str]) -> None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oidcauthlib
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.23
|
|
4
4
|
Summary: oidcauthlib
|
|
5
5
|
Author-email: Imran Qureshi <imran.qureshi@bwell.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -55,14 +55,17 @@ scopes = await manager.get_scopes_from_user_info_async(access_token=token)
|
|
|
55
55
|
`scope_claims`, then `group_claims` (each value taken whole). Commas separate like spaces, results are
|
|
56
56
|
de-duplicated, and `scope_remove_prefixes` are stripped (first match wins). Nothing beyond `scope`/`scp`
|
|
57
57
|
is read unless configured.
|
|
58
|
-
- **Provider** is matched to the token's `iss
|
|
59
|
-
|
|
60
|
-
never sent to another provider.
|
|
58
|
+
- **Provider** is matched to the token's `iss`: an exact `issuer`, else a `well_known_uri` on the same scheme,
|
|
59
|
+
host and port whose path segments start with the issuer's. Only a token with no `iss` at all is matched by
|
|
60
|
+
its client id. A token matching no provider raises `ValueError`; it is never sent to another provider.
|
|
61
|
+
- **Subject check:** the userinfo response's `sub` must equal the token's `sub` (OIDC Core 5.3.2); otherwise
|
|
62
|
+
the lookup raises `ValueError` and nothing is cached.
|
|
61
63
|
- **Cache** key is `oidc-userinfo-scopes:<issuer>:<client_id>:<sub>|v1` with each segment percent-encoded
|
|
62
64
|
(so values containing `:` cannot collide, and two providers never share an entry); only the
|
|
63
65
|
userinfo-derived scopes are stored, never the userinfo response. The token's own scopes are re-derived on every call. A token without
|
|
64
|
-
`sub
|
|
66
|
+
`sub` or without an issuer, or an empty scope list, is not cached. Changes to a user's entitlements are visible after at most
|
|
65
67
|
`cache_ttl_seconds`.
|
|
66
|
-
- **Failures:**
|
|
67
|
-
entry
|
|
68
|
-
or
|
|
68
|
+
- **Failures:** the cache never fails a lookup. A write failure is logged and swallowed; a read failure or a
|
|
69
|
+
malformed entry is logged and treated as a miss, and the refetched scopes overwrite the bad entry. A
|
|
70
|
+
userinfo HTTP error, a missing provider or a mismatched subject is raised, so each service decides whether
|
|
71
|
+
to fail closed or degrade.
|
oidcauthlib-3.0.22/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
3.0.22
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/middleware/request_scope_middleware.py
RENAMED
|
File without changes
|
{oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/middleware/token_reader_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/repository/memory/memory_repository.py
RENAMED
|
File without changes
|
|
File without changes
|
{oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/repository/mongo/mongo_repository.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/auth/well_known_configuration/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/container/oidc_authlib_container_factory.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/open_telemetry/filtering_span_exporter.py
RENAMED
|
File without changes
|
{oidcauthlib-3.0.22 → oidcauthlib-3.0.23}/oidcauthlib/open_telemetry/filtering_span_processor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|