Skip to content

Commit 219b7d6

Browse files
Fall back to jwks_url if no jwks_uri found
- Use `jwks_url` as a fallback if the `jwks_uri` is not defined in the configuration. This makes it possible to use a broader selection of JWT providers. - `OpenIDConfigurationTypeDef` was removed, you can use `ConfigurationTypeDef` instead. - Security updates to libraries (aiohttp, idna, cryptography). - Updated pre-commit hooks. - Improvements to README.
1 parent 2e07935 commit 219b7d6

File tree

10 files changed

+223
-148
lines changed

10 files changed

+223
-148
lines changed

.pre-commit-config.yaml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# See https://pre-commit.com/hooks.html for more hooks
33
repos:
44
- repo: https://github.com/pre-commit/pre-commit-hooks
5-
rev: v4.4.0
5+
rev: v4.6.0
66
hooks:
77
- id: trailing-whitespace
88
- id: end-of-file-fixer
@@ -21,27 +21,27 @@ repos:
2121
args: ["--fix=crlf"]
2222
files: \.bat$
2323
- repo: https://github.com/pycqa/isort
24-
rev: 5.12.0
24+
rev: 5.13.2
2525
hooks:
2626
- id: isort
2727
- repo: https://github.com/psf/black
28-
rev: 23.1.0
28+
rev: 24.8.0
2929
hooks:
3030
- id: black
3131
- repo: https://github.com/PyCQA/flake8
32-
rev: 6.0.0
32+
rev: 7.1.1
3333
hooks:
3434
- id: flake8
3535
- repo: https://github.com/pre-commit/mirrors-prettier
36-
rev: v2.7.1
36+
rev: v3.1.0
3737
hooks:
3838
- id: prettier
3939
- repo: https://github.com/PyCQA/bandit
40-
rev: 1.7.4
40+
rev: 1.7.9
4141
hooks:
4242
- id: bandit
4343
exclude: /tests
4444
- repo: https://github.com/twu/skjold
45-
rev: v0.6.1
45+
rev: v0.6.2
4646
hooks:
4747
- id: skjold

CHANGELOG.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,23 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
77

88
## [Unreleased]
99

10+
## [0.8.0] - 2024-08-07
11+
12+
### Added
13+
14+
- Use `jwks_url` as a fallback if the `jwks_uri` is not defined in the configuration.
15+
This makes it possible to use a broader selection of JWT providers.
16+
17+
### Removed
18+
19+
- `OpenIDConfigurationTypeDef` was removed, you can use `ConfigurationTypeDef` instead.
20+
21+
### Changed
22+
23+
- Security updates to libraries (aiohttp, idna, cryptography).
24+
- Updated pre-commit hooks.
25+
- Improvements to README.
26+
1027
## [0.7.0] - 2024-01-17
1128

1229
### Added
@@ -98,7 +115,8 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
98115

99116
- Everything for the initial release
100117

101-
[unreleased]: https://github.com/ioxiocom/pyjwt-key-fetcher/compare/0.7.0...HEAD
118+
[unreleased]: https://github.com/ioxiocom/pyjwt-key-fetcher/compare/0.8.0...HEAD
119+
[0.8.0]: https://github.com/ioxiocom/pyjwt-key-fetcher/compare/0.7.0...0.8.0
102120
[0.7.0]: https://github.com/ioxiocom/pyjwt-key-fetcher/compare/0.6.0...0.7.0
103121
[0.6.0]: https://github.com/ioxiocom/pyjwt-key-fetcher/compare/0.5.0...0.6.0
104122
[0.5.0]: https://github.com/ioxiocom/pyjwt-key-fetcher/compare/0.4.0...0.5.0

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ Async library to fetch JWKs for JWT tokens.
1010

1111
This library is intended to be used together with
1212
[PyJWT](https://pyjwt.readthedocs.io/en/stable/) to automatically verify keys signed by
13-
OpenID Connect providers. It retrieves the `iss` (issuer) and the `kid` (key ID) from
14-
the JWT, fetches the `.well-known/openid-configuration` from the issuer to find out the
15-
`jwks_uri` and fetches that to find the right key.
13+
for example OpenID Connect providers. It retrieves the `iss` (issuer) and the `kid` (key
14+
ID) from the JWT, fetches the configuration, typically from
15+
`.well-known/openid-configuration` (can be overridden) from the issuer to find out the
16+
`jwks_uri` (or `jwks_url`) and fetches that to find the right key.
1617

1718
This should give similar ability to verify keys as for example
1819
[https://jwt.io/](https://jwt.io/), where you can just paste in a token, and it will
@@ -95,7 +96,8 @@ The minimum interval for checking for new keys can for now not be adjusted.
9596
You can change from which path the configuration is loaded from the issuer (`iss`). By
9697
default, the configuration is assumed to be an OpenID Connect configuration and to be
9798
loaded from `/.well-known/openid-configuration`. As long as the configuration contains a
98-
`jwks_uri` you can change the configuration to be loaded from a custom path.
99+
`jwks_uri` or a `jwks_url` you can change the configuration to be loaded from a custom
100+
path.
99101

100102
You can override the config path when creating the `AsyncKeyFetcher` like this:
101103

@@ -107,7 +109,7 @@ AsyncKeyFetcher(config_path="/.well-known/dataspace/party-configuration.json")
107109

108110
If you use an issuer that does not provide a configuration (they are for example missing
109111
the `/.well-known/openid-configuration`), you can create a static configuration to use
110-
for that issuer instead and in it specify the `jwks_uri` like this:
112+
for that issuer instead and in it specify the `jwks_uri` (or `jwks_url`) like this:
111113

112114
```python
113115
AsyncKeyFetcher(

poetry.lock

Lines changed: 131 additions & 115 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyjwt_key_fetcher/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
from pyjwt_key_fetcher.fetcher import AsyncKeyFetcher
33
from pyjwt_key_fetcher.http_client import DefaultHTTPClient
44
from pyjwt_key_fetcher.key import Key
5-
from pyjwt_key_fetcher.provider import OpenIDConfigurationTypeDef
5+
from pyjwt_key_fetcher.provider import ConfigurationTypeDef

pyjwt_key_fetcher/fetcher.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pyjwt_key_fetcher.errors import JWTFormatError, JWTInvalidIssuerError
77
from pyjwt_key_fetcher.http_client import DefaultHTTPClient, HTTPClient
88
from pyjwt_key_fetcher.key import Key
9-
from pyjwt_key_fetcher.provider import OpenIDConfigurationTypeDef, Provider
9+
from pyjwt_key_fetcher.provider import ConfigurationTypeDef, Provider
1010

1111

1212
class AsyncKeyFetcher:
@@ -17,7 +17,7 @@ def __init__(
1717
cache_ttl: int = 3600,
1818
cache_maxsize: int = 32,
1919
config_path: str = "/.well-known/openid-configuration",
20-
static_issuer_config: Optional[Dict[str, OpenIDConfigurationTypeDef]] = None,
20+
static_issuer_config: Optional[Dict[str, ConfigurationTypeDef]] = None,
2121
) -> None:
2222
"""
2323
Initialize a new AsyncKeyFetcher.

pyjwt_key_fetcher/provider.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Callable, Dict, Mapping, Optional, TypedDict
1+
from typing import Any, Callable, Dict, Mapping, Optional, TypedDict, Union
22
from uuid import uuid4
33

44
import aiocache # type: ignore
@@ -37,21 +37,32 @@ def key_builder(
3737
return key
3838

3939

40-
class OpenIDConfigurationTypeDef(TypedDict):
40+
class JwksUriConfigurationTypeDef(TypedDict):
4141
"""
42-
Type definition for the OpenID configuration values relevant to JWT validation.
42+
Type definition for an OpenID Connect compatible configuration with a jwks_uri.
4343
"""
4444

4545
jwks_uri: str
4646

4747

48+
class JwksUrlConfigurationTypeDef(TypedDict):
49+
"""
50+
Type definition for a configuration using jwks_url instead of jwks_uri.
51+
"""
52+
53+
jwks_url: str
54+
55+
56+
ConfigurationTypeDef = Union[JwksUriConfigurationTypeDef, JwksUrlConfigurationTypeDef]
57+
58+
4859
class Provider:
4960
def __init__(
5061
self,
5162
iss: str,
5263
http_client: HTTPClient,
5364
config_path: str = "/.well-known/openid-configuration",
54-
static_config: Optional[OpenIDConfigurationTypeDef] = None,
65+
static_config: Optional[ConfigurationTypeDef] = None,
5566
) -> None:
5667
self.iss = iss
5768
self.http_client = http_client
@@ -85,17 +96,24 @@ async def get_configuration(self) -> Mapping[str, Any]:
8596

8697
async def _get_jwks_uri(self) -> str:
8798
"""
88-
Retrieve the uri to JWKs.
99+
Retrieve the uri/url to JWKs.
89100
90-
:return: The uri to the JWKs.
101+
:return: The uri/url to the JWKs.
91102
:raise JWTHTTPFetchError: If there's a problem fetching the data.
92-
:raise JWTProviderConfigError: If the config doesn't contain "jwks_uri".
103+
:raise JWTProviderConfigError: If the config doesn't contain "jwks_uri" or
104+
"jwks_url".
93105
"""
94106
conf = await self.get_configuration()
107+
jwks_uri: str
95108
try:
96-
jwks_uri: str = conf["jwks_uri"]
109+
jwks_uri = conf["jwks_uri"]
97110
except KeyError as e:
98-
raise JWTProviderConfigError("Missing 'jwks_uri' in configuration") from e
111+
try:
112+
jwks_uri = conf["jwks_url"]
113+
except KeyError:
114+
raise JWTProviderConfigError(
115+
"Missing 'jwks_uri' and 'jwks_url' in configuration"
116+
) from e
99117
return jwks_uri
100118

101119
@aiocache.cached(ttl=300, key_builder=key_builder)

pyjwt_key_fetcher/tests/conftest.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,14 @@ def __init__(
120120
provider: MockProvider,
121121
config_path: str = "/.well-known/openid-configuration",
122122
jwks_path: str = "/.well-known/jwks",
123+
jwks_uri_field: str = "jwks_uri",
123124
) -> None:
124125
self.providers = {provider.iss: provider}
125126
self.get_jwks = MagicMock(wraps=self.get_jwks) # type: ignore
126127
self.get_configuration = MagicMock(wraps=self.get_configuration) # type: ignore
127128
self.config_path = config_path
128129
self.jwks_path = jwks_path
130+
self.jwks_uri_field = jwks_uri_field
129131

130132
async def get_json(self, url: str) -> Dict[str, Any]:
131133
"""
@@ -159,7 +161,7 @@ def get_jwks(self, provider: MockProvider) -> Dict[str, Any]:
159161

160162
def get_configuration(self, provider: MockProvider) -> Dict[str, Any]:
161163
return {
162-
"jwks_uri": provider.iss + self.jwks_path,
164+
self.jwks_uri_field: provider.iss + self.jwks_path,
163165
}
164166

165167

@@ -169,9 +171,10 @@ async def _create(
169171
valid_issuers=None,
170172
iss: str = "https://example.com",
171173
aud: str = "default_audience",
174+
jwks_uri_field: str = "jwks_uri",
172175
):
173176
provider = MockProvider(iss=iss, aud=aud)
174-
http_client = MockHTTPClient(provider=provider)
177+
http_client = MockHTTPClient(provider=provider, jwks_uri_field=jwks_uri_field)
175178
fetcher = AsyncKeyFetcher(valid_issuers=valid_issuers, http_client=http_client)
176179
return provider, fetcher, http_client
177180

pyjwt_key_fetcher/tests/test_fetcher.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,18 @@
66
from pyjwt_key_fetcher.errors import JWTInvalidIssuerError, JWTKeyNotFoundError
77

88

9+
@pytest.mark.parametrize(
10+
"jwks_uri_field",
11+
[
12+
"jwks_uri",
13+
"jwks_url",
14+
],
15+
)
916
@pytest.mark.asyncio
10-
async def test_fetching_key(create_provider_fetcher_and_client):
11-
provider, fetcher, client = await create_provider_fetcher_and_client()
17+
async def test_fetching_key(create_provider_fetcher_and_client, jwks_uri_field):
18+
provider, fetcher, client = await create_provider_fetcher_and_client(
19+
jwks_uri_field=jwks_uri_field,
20+
)
1221

1322
token = provider.create_token()
1423
key_entry = await fetcher.get_key(token)
@@ -117,14 +126,23 @@ async def test_issuer_validation(create_provider_fetcher_and_client, create_prov
117126
assert client.get_jwks.call_count == 1
118127

119128

129+
@pytest.mark.parametrize(
130+
"jwks_uri_field",
131+
[
132+
"jwks_uri",
133+
"jwks_url",
134+
],
135+
)
120136
@pytest.mark.asyncio
121-
async def test_static_issuer_config():
137+
async def test_static_issuer_config(jwks_uri_field):
122138
issuer = "https://valid.example.com"
123139

124140
fetcher = pyjwt_key_fetcher.AsyncKeyFetcher(
125-
static_issuer_config={issuer: {"jwks_uri": f"{issuer}/.well-known/jwks.json"}}
141+
static_issuer_config={
142+
issuer: {jwks_uri_field: f"{issuer}/.well-known/jwks.json"}
143+
}
126144
)
127145
provider = fetcher._get_provider(issuer)
128146

129147
provider_config = await provider.get_configuration()
130-
assert provider_config == {"jwks_uri": f"{issuer}/.well-known/jwks.json"}
148+
assert provider_config == {jwks_uri_field: f"{issuer}/.well-known/jwks.json"}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ packages = [{ include = "pyjwt_key_fetcher", from = "." }]
1111
[tool.poetry.dependencies]
1212
python = "^3.8"
1313
PyJWT = { version = "^2.8.0", extras = ["crypto"] }
14-
aiohttp = { version = "^3.9.1", extras = ["speedups"] }
14+
aiohttp = {version = "^3.10.1", extras = ["speedups"]}
1515
cachetools = "^5.3.2"
1616
aiocache = "^0.12.2"
1717

0 commit comments

Comments
 (0)