oidcauthlib 3.0.22__tar.gz → 3.0.24__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 (96) hide show
  1. {oidcauthlib-3.0.22/oidcauthlib.egg-info → oidcauthlib-3.0.24}/PKG-INFO +11 -8
  2. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/README.md +10 -7
  3. oidcauthlib-3.0.24/VERSION +1 -0
  4. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/userinfo/oidc_userinfo_manager.py +81 -22
  5. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24/oidcauthlib.egg-info}/PKG-INFO +11 -8
  6. oidcauthlib-3.0.22/VERSION +0 -1
  7. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/LICENSE +0 -0
  8. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/MANIFEST.in +0 -0
  9. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/__init__.py +0 -0
  10. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/__init__.py +0 -0
  11. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/auth_helper.py +0 -0
  12. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/auth_manager.py +0 -0
  13. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/cache/__init__.py +0 -0
  14. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/cache/oauth_cache.py +0 -0
  15. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/cache/oauth_memory_cache.py +0 -0
  16. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/cache/oauth_mongo_cache.py +0 -0
  17. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/config/__init__.py +0 -0
  18. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/config/auth_config.py +0 -0
  19. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/config/auth_config_reader.py +0 -0
  20. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/dcr/__init__.py +0 -0
  21. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/dcr/dcr_client.py +0 -0
  22. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/dcr/dcr_manager.py +0 -0
  23. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/dcr/dcr_registration.py +0 -0
  24. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/exceptions/__init__.py +0 -0
  25. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/exceptions/authorization_bearer_token_expired_exception.py +0 -0
  26. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/exceptions/authorization_bearer_token_invalid_exception.py +0 -0
  27. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/exceptions/authorization_bearer_token_missing_exception.py +0 -0
  28. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/exceptions/authorization_needed_exception.py +0 -0
  29. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/fastapi_auth_manager.py +0 -0
  30. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/middleware/__init__.py +0 -0
  31. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/middleware/request_scope_middleware.py +0 -0
  32. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/middleware/token_reader_middleware.py +0 -0
  33. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/models/__init__.py +0 -0
  34. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/models/auth.py +0 -0
  35. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/models/base_db_model.py +0 -0
  36. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/models/cache_item.py +0 -0
  37. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/models/client_key_set.py +0 -0
  38. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/models/token.py +0 -0
  39. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/repository/__init__.py +0 -0
  40. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/repository/base_repository.py +0 -0
  41. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/repository/memory/__init__.py +0 -0
  42. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/repository/memory/memory_repository.py +0 -0
  43. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/repository/mongo/__init__.py +0 -0
  44. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/repository/mongo/mongo_repository.py +0 -0
  45. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/repository/repository_factory.py +0 -0
  46. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/routers/__init__.py +0 -0
  47. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/routers/auth_router.py +0 -0
  48. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/token_reader.py +0 -0
  49. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/userinfo/__init__.py +0 -0
  50. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/well_known_configuration/__init__.py +0 -0
  51. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/well_known_configuration/auth_server_metadata.py +0 -0
  52. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/well_known_configuration/auth_server_metadata_discovery.py +0 -0
  53. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/well_known_configuration/well_known_configuration_cache.py +0 -0
  54. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/well_known_configuration/well_known_configuration_cache_result.py +0 -0
  55. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/auth/well_known_configuration/well_known_configuration_manager.py +0 -0
  56. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/cache_gate/__init__.py +0 -0
  57. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/cache_gate/advisory_lock.py +0 -0
  58. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/cache_gate/version_gate.py +0 -0
  59. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/container/__init__.py +0 -0
  60. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/container/oidc_authlib_container_factory.py +0 -0
  61. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/open_telemetry/__init__.py +0 -0
  62. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/open_telemetry/attribute_names.py +0 -0
  63. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/open_telemetry/configurator.py +0 -0
  64. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/open_telemetry/filtering_sampler.py +0 -0
  65. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/open_telemetry/filtering_span_exporter.py +0 -0
  66. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/open_telemetry/filtering_span_processor.py +0 -0
  67. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/open_telemetry/otel_setup.py +0 -0
  68. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/open_telemetry/span_names.py +0 -0
  69. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/py.typed +0 -0
  70. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/storage/__init__.py +0 -0
  71. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/storage/cache_to_collection_mapper.py +0 -0
  72. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/storage/memory_storage_factory.py +0 -0
  73. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/storage/mongo_gridfs_db.py +0 -0
  74. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/storage/mongo_gridfs_exception.py +0 -0
  75. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/storage/mongo_storage_factory.py +0 -0
  76. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/storage/storage_factory.py +0 -0
  77. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/storage/storage_factory_creator.py +0 -0
  78. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/utilities/__init__.py +0 -0
  79. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/utilities/cached.py +0 -0
  80. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/utilities/environment/__init__.py +0 -0
  81. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/utilities/environment/abstract_environment_variables.py +0 -0
  82. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/utilities/environment/oidc_environment_variables.py +0 -0
  83. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/utilities/logger/__init__.py +0 -0
  84. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/utilities/logger/log_levels.py +0 -0
  85. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/utilities/logger/logging_response.py +0 -0
  86. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/utilities/logger/logging_transport.py +0 -0
  87. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/utilities/mongo_url_utils.py +0 -0
  88. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib/utilities/url_validator.py +0 -0
  89. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib.egg-info/SOURCES.txt +0 -0
  90. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib.egg-info/dependency_links.txt +0 -0
  91. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib.egg-info/requires.txt +0 -0
  92. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/oidcauthlib.egg-info/top_level.txt +0 -0
  93. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/pyproject.toml +0 -0
  94. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/setup.cfg +0 -0
  95. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/tests/test_mongo_url_utils.py +0 -0
  96. {oidcauthlib-3.0.22 → oidcauthlib-3.0.24}/tests/test_simple.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oidcauthlib
3
- Version: 3.0.22
3
+ Version: 3.0.24
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` (exact `issuer`, then `well_known_uri` equal to or beneath the
59
- issuer at a path boundary), then its client id. A token matching no provider raises `ValueError`; it is
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:** for a token that has a `sub`, the userinfo response must carry the same `sub`
62
+ (OIDC Core 5.3.2); a missing or different one 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`, or an empty scope list, is not cached. Changes to a user's entitlements are visible after at most
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:** a cache write failure is logged and swallowed. A cache read failure, malformed cache
67
- entry, userinfo HTTP error or missing provider is raised, so each service decides whether to fail closed
68
- or degrade.
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` (exact `issuer`, then `well_known_uri` equal to or beneath the
31
- issuer at a path boundary), then its client id. A token matching no provider raises `ValueError`; it is
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:** for a token that has a `sub`, the userinfo response must carry the same `sub`
34
+ (OIDC Core 5.3.2); a missing or different one 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`, or an empty scope list, is not cached. Changes to a user's entitlements are visible after at most
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:** a cache write failure is logged and swallowed. A cache read failure, malformed cache
39
- entry, userinfo HTTP error or missing provider is raised, so each service decides whether to fail closed
40
- or degrade.
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.24
@@ -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``, then ``iss`` contained in the
41
- provider's ``well_known_uri``), then to its client id. A token matching no provider is an error:
42
- the bearer token is never sent to an unrelated provider.
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
+ - For a token that has a ``sub``, the userinfo response must carry the same ``sub`` (OIDC Core 5.3.2); a
47
+ missing or different one fails the lookup 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 the client.
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
- - A cache write failure is logged and swallowed; it never fails the lookup.
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, unreadable cache entry) are raised so
64
- the caller decides whether to fail closed or degrade.
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's issuer or client id.
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 issuer {issuer!r} or client id {client_id!r}")
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 itself or a path beneath it.
154
+ """True when ``uri`` is on the issuer's origin and its path is the issuer's path or beneath it.
135
155
 
136
- A bare substring test would also match ``https://a.example.com.au/...`` for the issuer
137
- ``https://a.example.com`` and send the token to the wrong provider.
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
- base = issuer.rstrip("/")
140
- return uri == base or uri.startswith(f"{base}/")
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,13 @@ 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 must carry a ``sub`` equal to the token's ``sub``; a response without
188
+ one, or with a different one, cannot be shown to describe the token's user and is rejected. A token
189
+ that has no ``sub`` has nothing to compare, so its response is accepted (and never cached).
190
+
152
191
  Raises:
153
- ValueError: the token, auth config, discovery document or response is unusable.
192
+ ValueError: the token, auth config, discovery document or response is unusable, or the response's
193
+ subject is missing or differs from the token's.
154
194
  httpx.HTTPError: the userinfo request failed.
155
195
  """
156
196
  if not access_token.token:
@@ -182,6 +222,10 @@ class OidcUserInfoManager:
182
222
  user_info = response.json()
183
223
  if not isinstance(user_info, dict):
184
224
  raise ValueError("User info response is not a valid JSON object")
225
+ if access_token.subject and user_info.get("sub") != access_token.subject:
226
+ # Missing or different: ``sub`` is required in the response, and one that cannot be shown
227
+ # to be the token's subject must not be turned into scopes. Neither subject is logged.
228
+ raise ValueError("User info response subject is missing or differs from the token's subject")
185
229
  return user_info
186
230
 
187
231
  async def get_scopes_from_user_info_async(self, *, access_token: Token) -> list[str]:
@@ -190,7 +234,7 @@ class OidcUserInfoManager:
190
234
  The userinfo scopes come from the cache when present, else from the endpoint (then cached).
191
235
 
192
236
  Raises:
193
- ValueError: the cached entry has an unexpected shape, or no provider matches the token.
237
+ ValueError: no provider matches the token, or the userinfo response is for another subject.
194
238
  httpx.HTTPError: the userinfo request failed.
195
239
  """
196
240
  tracer = trace.get_tracer(__name__)
@@ -264,18 +308,33 @@ class OidcUserInfoManager:
264
308
  return f"{':'.join([cls._KEY_PREFIX, *segments])}|v{cls.CACHE_VERSION}"
265
309
 
266
310
  async def _read_cached_scopes(self, *, cache_key: str) -> list[str] | None:
267
- raw = await self._store.get(cache_key, collection=self._cache_collection)
311
+ """Cached userinfo scopes, or None for a miss.
312
+
313
+ A store failure or an entry of the wrong shape is logged and treated as a miss: the cache is an
314
+ optimization, so one bad entry (or a store outage) must not fail every request for that key until the
315
+ TTL expires. The refetched scopes then overwrite the bad entry.
316
+ """
268
317
  key_id = self._loggable(cache_key=cache_key)
318
+ try:
319
+ raw = await self._store.get(cache_key, collection=self._cache_collection)
320
+ except Exception:
321
+ logger.exception(f"Failed to read OIDC userinfo scopes from the cache for key: {key_id}")
322
+ return None
269
323
  if raw is None:
270
324
  logger.debug(f"OIDC userinfo scopes cache miss for key: {key_id}")
271
325
  return None
272
326
  try:
273
327
  entry = OidcUserInfoCacheEntry.model_validate(raw)
274
- except ValueError as exc:
275
- raise ValueError(f"OIDC userinfo scopes cache entry has invalid shape for key: {key_id}") from exc
328
+ except ValueError:
329
+ logger.warning(f"OIDC userinfo scopes cache entry has invalid shape for key: {key_id}; refetching")
330
+ return None
276
331
  scopes = entry.value.get("scopes")
277
332
  if not isinstance(scopes, list) or not all(isinstance(s, str) for s in scopes):
278
- raise ValueError(f"OIDC userinfo scopes cache entry has invalid shape for key: {key_id}. {type(scopes)}")
333
+ logger.warning(
334
+ f"OIDC userinfo scopes cache entry has invalid scopes ({type(scopes).__name__}) for key: {key_id}; "
335
+ "refetching"
336
+ )
337
+ return None
279
338
  return scopes
280
339
 
281
340
  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.22
3
+ Version: 3.0.24
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` (exact `issuer`, then `well_known_uri` equal to or beneath the
59
- issuer at a path boundary), then its client id. A token matching no provider raises `ValueError`; it is
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:** for a token that has a `sub`, the userinfo response must carry the same `sub`
62
+ (OIDC Core 5.3.2); a missing or different one 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`, or an empty scope list, is not cached. Changes to a user's entitlements are visible after at most
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:** a cache write failure is logged and swallowed. A cache read failure, malformed cache
67
- entry, userinfo HTTP error or missing provider is raised, so each service decides whether to fail closed
68
- or degrade.
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.
@@ -1 +0,0 @@
1
- 3.0.22
File without changes
File without changes
File without changes