xtr-security-jwt 3.0.0__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 (92) hide show
  1. xtr_security_jwt-3.0.0/LICENSE +21 -0
  2. xtr_security_jwt-3.0.0/PKG-INFO +370 -0
  3. xtr_security_jwt-3.0.0/README.md +340 -0
  4. xtr_security_jwt-3.0.0/pyproject.toml +170 -0
  5. xtr_security_jwt-3.0.0/pyproject.toml.orig +149 -0
  6. xtr_security_jwt-3.0.0/src/xtr_security_jwt/.agents/skills/xtr-security-jwt/SKILL.md +253 -0
  7. xtr_security_jwt-3.0.0/src/xtr_security_jwt/.agents/skills/xtr-security-jwt/references/configuration.md +75 -0
  8. xtr_security_jwt-3.0.0/src/xtr_security_jwt/.agents/skills/xtr-security-jwt/references/events-and-errors.md +48 -0
  9. xtr_security_jwt-3.0.0/src/xtr_security_jwt/__init__.py +8 -0
  10. xtr_security_jwt-3.0.0/src/xtr_security_jwt/bundle/__init__.py +29 -0
  11. xtr_security_jwt-3.0.0/src/xtr_security_jwt/bundle/encoder_config.py +31 -0
  12. xtr_security_jwt-3.0.0/src/xtr_security_jwt/bundle/jwt_authenticator_config.py +28 -0
  13. xtr_security_jwt-3.0.0/src/xtr_security_jwt/bundle/jwt_bundle.py +257 -0
  14. xtr_security_jwt-3.0.0/src/xtr_security_jwt/bundle/jwt_config.py +67 -0
  15. xtr_security_jwt-3.0.0/src/xtr_security_jwt/bundle/jwt_user_provider_config.py +29 -0
  16. xtr_security_jwt-3.0.0/src/xtr_security_jwt/bundle/token_extractors_configs.py +97 -0
  17. xtr_security_jwt-3.0.0/src/xtr_security_jwt/command/__init__.py +9 -0
  18. xtr_security_jwt-3.0.0/src/xtr_security_jwt/command/_registry.py +15 -0
  19. xtr_security_jwt-3.0.0/src/xtr_security_jwt/command/check_config_command.py +45 -0
  20. xtr_security_jwt-3.0.0/src/xtr_security_jwt/command/generate_key_pair_command.py +90 -0
  21. xtr_security_jwt-3.0.0/src/xtr_security_jwt/command/generate_token_command.py +99 -0
  22. xtr_security_jwt-3.0.0/src/xtr_security_jwt/encoder/__init__.py +13 -0
  23. xtr_security_jwt-3.0.0/src/xtr_security_jwt/encoder/default_jwt_encoder.py +101 -0
  24. xtr_security_jwt-3.0.0/src/xtr_security_jwt/encoder/header_aware_jwt_encoder_interface.py +38 -0
  25. xtr_security_jwt-3.0.0/src/xtr_security_jwt/encoder/jwt_encoder_interface.py +37 -0
  26. xtr_security_jwt-3.0.0/src/xtr_security_jwt/event/__init__.py +27 -0
  27. xtr_security_jwt-3.0.0/src/xtr_security_jwt/event/authentication_failure_event.py +62 -0
  28. xtr_security_jwt-3.0.0/src/xtr_security_jwt/event/authentication_success_event.py +42 -0
  29. xtr_security_jwt-3.0.0/src/xtr_security_jwt/event/jwt_authenticated_event.py +41 -0
  30. xtr_security_jwt-3.0.0/src/xtr_security_jwt/event/jwt_created_event.py +56 -0
  31. xtr_security_jwt-3.0.0/src/xtr_security_jwt/event/jwt_decoded_event.py +44 -0
  32. xtr_security_jwt-3.0.0/src/xtr_security_jwt/event/jwt_encoded_event.py +28 -0
  33. xtr_security_jwt-3.0.0/src/xtr_security_jwt/event/jwt_expired_event.py +19 -0
  34. xtr_security_jwt-3.0.0/src/xtr_security_jwt/event/jwt_failure_event_interface.py +33 -0
  35. xtr_security_jwt-3.0.0/src/xtr_security_jwt/event/jwt_invalid_event.py +19 -0
  36. xtr_security_jwt-3.0.0/src/xtr_security_jwt/event/jwt_not_found_event.py +18 -0
  37. xtr_security_jwt-3.0.0/src/xtr_security_jwt/events.py +62 -0
  38. xtr_security_jwt-3.0.0/src/xtr_security_jwt/exception/__init__.py +30 -0
  39. xtr_security_jwt-3.0.0/src/xtr_security_jwt/exception/expired_token_error.py +20 -0
  40. xtr_security_jwt-3.0.0/src/xtr_security_jwt/exception/invalid_payload_error.py +30 -0
  41. xtr_security_jwt-3.0.0/src/xtr_security_jwt/exception/invalid_token_error.py +20 -0
  42. xtr_security_jwt-3.0.0/src/xtr_security_jwt/exception/jwt_decode_failure_error.py +28 -0
  43. xtr_security_jwt-3.0.0/src/xtr_security_jwt/exception/jwt_encode_failure_error.py +24 -0
  44. xtr_security_jwt-3.0.0/src/xtr_security_jwt/exception/jwt_failure_error.py +47 -0
  45. xtr_security_jwt-3.0.0/src/xtr_security_jwt/exception/missing_claim_error.py +39 -0
  46. xtr_security_jwt-3.0.0/src/xtr_security_jwt/exception/missing_token_error.py +20 -0
  47. xtr_security_jwt-3.0.0/src/xtr_security_jwt/factory/__init__.py +7 -0
  48. xtr_security_jwt-3.0.0/src/xtr_security_jwt/factory/jwt_authenticator_factory.py +182 -0
  49. xtr_security_jwt-3.0.0/src/xtr_security_jwt/py.typed +0 -0
  50. xtr_security_jwt-3.0.0/src/xtr_security_jwt/recipe/__init__.py +16 -0
  51. xtr_security_jwt-3.0.0/src/xtr_security_jwt/recipe/files/config/jwt.py.tmpl +28 -0
  52. xtr_security_jwt-3.0.0/src/xtr_security_jwt/recipe/manifest.toml +36 -0
  53. xtr_security_jwt-3.0.0/src/xtr_security_jwt/response/__init__.py +7 -0
  54. xtr_security_jwt-3.0.0/src/xtr_security_jwt/response/jwt_authentication_failure_response.py +33 -0
  55. xtr_security_jwt-3.0.0/src/xtr_security_jwt/security/__init__.py +5 -0
  56. xtr_security_jwt-3.0.0/src/xtr_security_jwt/security/authenticator/__init__.py +7 -0
  57. xtr_security_jwt-3.0.0/src/xtr_security_jwt/security/authenticator/jwt_authenticator.py +257 -0
  58. xtr_security_jwt-3.0.0/src/xtr_security_jwt/security/authenticator/token/__init__.py +7 -0
  59. xtr_security_jwt-3.0.0/src/xtr_security_jwt/security/authenticator/token/jwt_post_authentication_token.py +42 -0
  60. xtr_security_jwt-3.0.0/src/xtr_security_jwt/security/user/__init__.py +9 -0
  61. xtr_security_jwt-3.0.0/src/xtr_security_jwt/security/user/jwt_user.py +58 -0
  62. xtr_security_jwt-3.0.0/src/xtr_security_jwt/security/user/jwt_user_interface.py +28 -0
  63. xtr_security_jwt-3.0.0/src/xtr_security_jwt/security/user/jwt_user_provider.py +66 -0
  64. xtr_security_jwt-3.0.0/src/xtr_security_jwt/services/__init__.py +13 -0
  65. xtr_security_jwt-3.0.0/src/xtr_security_jwt/services/jws_provider/__init__.py +8 -0
  66. xtr_security_jwt-3.0.0/src/xtr_security_jwt/services/jws_provider/joserfc_jws_provider.py +208 -0
  67. xtr_security_jwt-3.0.0/src/xtr_security_jwt/services/jws_provider/jws_provider_interface.py +36 -0
  68. xtr_security_jwt-3.0.0/src/xtr_security_jwt/services/jwt_manager.py +153 -0
  69. xtr_security_jwt-3.0.0/src/xtr_security_jwt/services/jwt_token_manager_interface.py +53 -0
  70. xtr_security_jwt-3.0.0/src/xtr_security_jwt/services/key_loader/__init__.py +15 -0
  71. xtr_security_jwt-3.0.0/src/xtr_security_jwt/services/key_loader/_algorithms.py +75 -0
  72. xtr_security_jwt-3.0.0/src/xtr_security_jwt/services/key_loader/abstract_key_loader.py +111 -0
  73. xtr_security_jwt-3.0.0/src/xtr_security_jwt/services/key_loader/key_dumper_interface.py +20 -0
  74. xtr_security_jwt-3.0.0/src/xtr_security_jwt/services/key_loader/key_loader_interface.py +47 -0
  75. xtr_security_jwt-3.0.0/src/xtr_security_jwt/services/key_loader/raw_key_loader.py +84 -0
  76. xtr_security_jwt-3.0.0/src/xtr_security_jwt/services/payload_enrichment/__init__.py +9 -0
  77. xtr_security_jwt-3.0.0/src/xtr_security_jwt/services/payload_enrichment/chain_enrichment.py +38 -0
  78. xtr_security_jwt-3.0.0/src/xtr_security_jwt/services/payload_enrichment/null_enrichment.py +26 -0
  79. xtr_security_jwt-3.0.0/src/xtr_security_jwt/services/payload_enrichment/random_jti_enrichment.py +32 -0
  80. xtr_security_jwt-3.0.0/src/xtr_security_jwt/services/payload_enrichment_interface.py +24 -0
  81. xtr_security_jwt-3.0.0/src/xtr_security_jwt/signature/__init__.py +8 -0
  82. xtr_security_jwt-3.0.0/src/xtr_security_jwt/signature/created_jws.py +36 -0
  83. xtr_security_jwt-3.0.0/src/xtr_security_jwt/signature/loaded_jws.py +123 -0
  84. xtr_security_jwt-3.0.0/src/xtr_security_jwt/token_extractor/__init__.py +19 -0
  85. xtr_security_jwt-3.0.0/src/xtr_security_jwt/token_extractor/authorization_header_token_extractor.py +45 -0
  86. xtr_security_jwt-3.0.0/src/xtr_security_jwt/token_extractor/chain_token_extractor.py +52 -0
  87. xtr_security_jwt-3.0.0/src/xtr_security_jwt/token_extractor/cookie_token_extractor.py +34 -0
  88. xtr_security_jwt-3.0.0/src/xtr_security_jwt/token_extractor/query_parameter_token_extractor.py +34 -0
  89. xtr_security_jwt-3.0.0/src/xtr_security_jwt/token_extractor/split_cookie_extractor.py +45 -0
  90. xtr_security_jwt-3.0.0/src/xtr_security_jwt/token_extractor/token_extractor_interface.py +24 -0
  91. xtr_security_jwt-3.0.0/src/xtr_security_jwt/user_provider/__init__.py +7 -0
  92. xtr_security_jwt-3.0.0/src/xtr_security_jwt/user_provider/jwt_user_factory.py +52 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xterr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,370 @@
1
+ Metadata-Version: 2.4
2
+ Name: xtr-security-jwt
3
+ Version: 3.0.0
4
+ Summary: Self-issued JSON Web Tokens for xtr security: key sets, an encoder and a token manager for a user.
5
+ Keywords: security,jwt,jose,authentication,token
6
+ Author: Xterr
7
+ Author-email: Xterr <me@xterr.dev>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Typing :: Typed
17
+ Requires-Dist: xtr-security-core>=3.0,<4
18
+ Requires-Dist: xtr-security-http>=3.0,<4
19
+ Requires-Dist: joserfc>=1.7
20
+ Requires-Dist: xtr-clock>=3.0,<4
21
+ Requires-Dist: xtr-event-dispatcher-contracts>=3.0,<4
22
+ Requires-Dist: xtr-event-dispatcher>=3.0,<4
23
+ Requires-Dist: typing-extensions>=4.12
24
+ Requires-Dist: xtr-dependency-injection>=3.0,<4
25
+ Requires-Dist: xtr-security>=3.0,<4
26
+ Requires-Dist: xtr-console>=3.0,<4 ; extra == 'console'
27
+ Requires-Python: >=3.11
28
+ Provides-Extra: console
29
+ Description-Content-Type: text/markdown
30
+
31
+ <div align="center">
32
+
33
+ # xtr-security-jwt
34
+
35
+ **Self-issued JSON Web Tokens for xtr security: an encoder, a token manager and a firewall authenticator.**
36
+
37
+ <img alt="python 3.11+" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776AB?logo=python&logoColor=white">
38
+ <img alt="typed" src="https://img.shields.io/badge/typed-ty%20%2B%20basedpyright-1f6feb">
39
+ <img alt="license MIT" src="https://img.shields.io/badge/license-MIT-blue">
40
+
41
+ </div>
42
+
43
+ ---
44
+
45
+ ## Why?
46
+
47
+ The [security family](../xtr-security) verifies bearer tokens issued elsewhere, but issues none
48
+ of its own. This package is the self-issued-token layer on top of it: an application signs a
49
+ JSON Web Token for one of its users, hands it to a client, and accepts it back on a protected
50
+ route — the whole loop, with no authorization server.
51
+
52
+ It ships everything for both halves of that loop:
53
+
54
+ - 🔑 **A key loader** — a signing key given as text or a file path, an optional pass phrase, a
55
+ public key derived from the private one when absent, and extra public keys other issuers are
56
+ trusted by.
57
+ - ✍️ **An encoder and a token manager** — the encoder signs and verifies claims through a JWS
58
+ provider; the token manager assembles the user's roles and identity, announces the claims for
59
+ a listener to shape, signs them, and never reads any of the token from client input.
60
+ - 🔥 **A firewall key `jwt`** — one line in a firewall accepts self-issued tokens; the bundle
61
+ wires the authenticator, the extractors and the user provider through the security family's
62
+ seams.
63
+ - 🪪 **A stateless user** — a user rebuilt from a token's own claims, so a deployment that keeps
64
+ no user store still authenticates a request by the token alone.
65
+ - 🛠️ **Commands** — mint a signing key pair, mint a token for a user, or check that the
66
+ configured keys sign and verify.
67
+
68
+ ## Install
69
+
70
+ ```sh
71
+ uv add xtr-security-jwt # the library and its JwtBundle
72
+ uv add "xtr-security-jwt[console]" # + jwt:generate-keypair, jwt:generate-token, jwt:check-config
73
+ ```
74
+
75
+ Requires Python 3.11+. The package ships a bundle, so it depends on the container and the
76
+ security bundle it wires into, alongside the security family's core and http edge and the event
77
+ dispatcher and clock — all come with it.
78
+
79
+ ## Quick start
80
+
81
+ ### 1. Mint a signing key
82
+
83
+ ```console
84
+ $ jwt:generate-keypair --algorithm RS256 --output-dir secrets/
85
+ Wrote secrets/8f2c….pem and secrets/8f2c….jwks.json
86
+ ```
87
+
88
+ The private PEM signs; the public JWK set verifies — publish it, keep the PEM secret.
89
+
90
+ ### 2. Configure the signing key and a firewall
91
+
92
+ ```python
93
+ # app/config/jwt.py
94
+ from xtr_dependency_injection import configure, env
95
+ from xtr_security_jwt.bundle import JwtConfig
96
+
97
+
98
+ @configure
99
+ def jwt() -> JwtConfig:
100
+ return JwtConfig(secret_key=env("file:JWT_SECRET_KEY_PATH"), user_id_claim="username")
101
+ ```
102
+
103
+ ```python
104
+ # app/config/security.py
105
+ from xtr_dependency_injection import configure
106
+ from xtr_security.bundle import (
107
+ AccessControlConfig,
108
+ FirewallConfig,
109
+ SecurityConfig,
110
+ )
111
+ from xtr_security_jwt.bundle import JwtAuthenticatorConfig, JwtUserProviderConfig
112
+
113
+
114
+ @configure
115
+ def security() -> SecurityConfig:
116
+ return SecurityConfig(
117
+ providers={"jwt_users": JwtUserProviderConfig()},
118
+ firewalls={
119
+ "api": FirewallConfig(
120
+ pattern=r"^/api",
121
+ provider="jwt_users",
122
+ authenticators=(JwtAuthenticatorConfig(),),
123
+ ),
124
+ },
125
+ access_control=(AccessControlConfig(path=r"^/api", attribute="IS_AUTHENTICATED"),),
126
+ )
127
+ ```
128
+
129
+ `JwtAuthenticatorConfig()` is the whole of what a firewall needs to accept self-issued tokens:
130
+ the keys, the algorithm and the extractors all come from the JWT bundle's own configuration.
131
+ `JwtUserProviderConfig()` is a stateless provider that rebuilds the user from the token's claims,
132
+ for a deployment that keeps no user store; a firewall with its own user provider names that
133
+ instead.
134
+
135
+ ### 3. Protect the routes and write a `/token` endpoint
136
+
137
+ ```python
138
+ # app/web.py
139
+ from typing import Annotated
140
+
141
+ from fastapi import FastAPI
142
+ from xtr_dependency_injection import Injected, Kernel
143
+ from xtr_http_kernel import setup
144
+ from xtr_security_core.user.user_interface import UserInterface
145
+ from xtr_security_http import CurrentUser, Firewall
146
+ from xtr_security_jwt import JwtTokenManagerInterface
147
+
148
+ from app.bundles import BUNDLES
149
+
150
+ app = FastAPI(dependencies=[Firewall()])
151
+
152
+
153
+ @app.post("/token")
154
+ async def issue_token(
155
+ identifier: str,
156
+ tokens: Injected[JwtTokenManagerInterface],
157
+ ) -> dict[str, str]:
158
+ user = await _authenticated_user(identifier) # the application checks the password
159
+ return {"access_token": await tokens.create(user)}
160
+
161
+
162
+ @app.get("/api/me")
163
+ async def me(user: Annotated[UserInterface, CurrentUser()]) -> dict[str, str]:
164
+ return {"user": user.get_user_identifier()}
165
+
166
+
167
+ kernel = Kernel("app", concurrent_scoped_access=True)
168
+ setup(app, kernel)
169
+ ```
170
+
171
+ The token manager assembles the registered claims and signs them; it never reads any of the
172
+ token from client input. The application authenticates the user before calling `create`, the
173
+ FastAPI-tutorial way — burning a dummy hash for an unknown user, and throttling the endpoint
174
+ with a `RateLimited` dependency.
175
+
176
+ ```console
177
+ $ curl -i localhost:8000/api/me
178
+ HTTP/1.1 401 Unauthorized
179
+ WWW-Authenticate: Bearer
180
+
181
+ {"code":401,"message":"JWT Token not found"}
182
+
183
+ $ TOKEN=$(curl -s -X POST 'localhost:8000/token?identifier=ada' | jq -r .access_token)
184
+ $ curl -i -H "Authorization: Bearer $TOKEN" localhost:8000/api/me
185
+ HTTP/1.1 200 OK
186
+
187
+ {"user":"ada"}
188
+ ```
189
+
190
+ ## Configure
191
+
192
+ `JwtConfig` is a frozen dataclass buildable with no arguments, but the JWT bundle is an add-on
193
+ that cannot sign a token without a key: the build fails with an `InvalidConfigurationError`
194
+ naming the missing setting and the `<app>/config/jwt.py` `@configure` function when no
195
+ `secret_key` is configured.
196
+
197
+ | Field | Default | What it is |
198
+ |---|---|---|
199
+ | `secret_key` | `None` | The private key or shared secret that signs, as the key text or a file path. Required |
200
+ | `public_key` | `None` | The public key that verifies, or `None` to derive it from the private key |
201
+ | `additional_public_keys` | `()` | Files of extra public keys a token may also be verified against |
202
+ | `pass_phrase` | `""` | The pass phrase the private key is encrypted with |
203
+ | `token_ttl` | `3600` | How long a minted token lives, in seconds |
204
+ | `allow_no_expiration` | `False` | Whether a token with no expiry is honoured |
205
+ | `clock_skew` | `0` | Seconds of clock skew tolerated on a verified token's time claims |
206
+ | `encoder` | `EncoderConfig()` | The signature algorithm (`RS256` by default) and an optional encoder service override |
207
+ | `user_id_claim` | `"username"` | The claim the user's identifier is written into and read back from |
208
+ | `token_extractors` | `TokenExtractorsConfig()` | Where a firewall reads a token from |
209
+
210
+ A **token extractor** is one of four, told apart by where a token travels — the authorization
211
+ header is on by default, the others opt in:
212
+
213
+ | Extractor | Default | Reads |
214
+ |---|---|---|
215
+ | `authorization_header` | on, `Bearer` / `Authorization` | A token from a header, after a scheme prefix |
216
+ | `cookie` | off, `BEARER` | A token from a named cookie |
217
+ | `query_parameter` | off, `bearer` | A token from a named query parameter |
218
+ | `split_cookie` | off | A token split across several named cookies, rejoined |
219
+
220
+ A firewall authenticator is `JwtAuthenticatorConfig(provider=None, authenticator=None)` — an
221
+ optional user-provider override and an optional registered authenticator service to build
222
+ instead of the default. A stateless user provider is `JwtUserProviderConfig(user_class=JwtUser)`
223
+ — the class the provider rebuilds from a token's claims.
224
+
225
+ ## Services
226
+
227
+ `JwtBundle` registers, from the signing key down:
228
+
229
+ | Service | Interface | What it does |
230
+ |---|---|---|
231
+ | `RawKeyLoader` | `KeyLoaderInterface` | Reads the signing and verifying key material |
232
+ | `JoserfcJwsProvider` | `JwsProviderInterface` | Signs a payload and verifies a token with joserfc |
233
+ | `DefaultJwtEncoder` | `JwtEncoderInterface` | Maps the provider's outcome to encode/decode failures |
234
+ | `JwtManager` | `JwtTokenManagerInterface` | Assembles, announces and signs a token, reads one back |
235
+
236
+ The token manager gathers every registered `PayloadEnrichmentInterface` into a chain, so a
237
+ deployment stamps a claim onto every token by registering an enrichment — a `RandomJtiEnrichment`
238
+ for a unique id — never by reaching for the container.
239
+
240
+ ## Events
241
+
242
+ Every event carries a name constant on `Events`, so a listener names the constant rather than
243
+ importing the event class:
244
+
245
+ | Event | Name | Dispatched |
246
+ |---|---|---|
247
+ | `JwtCreatedEvent` | `Events.JWT_CREATED` | Before a token is signed; a listener may shape the claims and header |
248
+ | `JwtEncodedEvent` | `Events.JWT_ENCODED` | After a token is signed |
249
+ | `JwtDecodedEvent` | `Events.JWT_DECODED` | After a token verifies; a listener may reject it |
250
+ | `JwtAuthenticatedEvent` | `Events.JWT_AUTHENTICATED` | After a token authenticated a request |
251
+ | `JwtExpiredEvent` | `Events.JWT_EXPIRED` | When an expired token is refused |
252
+ | `JwtInvalidEvent` | `Events.JWT_INVALID` | When a bad token is refused |
253
+ | `JwtNotFoundEvent` | `Events.JWT_NOT_FOUND` | When a request carried no token |
254
+
255
+ ## Commands
256
+
257
+ With the console extra and a console bundle active:
258
+
259
+ | Command | Does |
260
+ |---|---|
261
+ | `jwt:generate-keypair [--algorithm RS256\|ES256\|EdDSA] [--kid KID] [--output-dir DIR]` | Mints a signing key and prints its private PEM and public JWK set — or writes them to `DIR`, named by the key id |
262
+ | `jwt:generate-token IDENTIFIER [--provider NAME]` | Loads the user by identifier through a configured user provider and prints a token signed for it; `--provider` picks between several |
263
+ | `jwt:check-config` | Signs a probe token and reads it back, proving the configured keys sign and verify |
264
+
265
+ ## Use in an application
266
+
267
+ Everything adding this package to an application on
268
+ [xtr-dependency-injection](../xtr-dependency-injection) takes — and, read backwards, what
269
+ removing it undoes.
270
+
271
+ - **Install** — `uv add xtr-security-jwt`; add `[console]` for the commands.
272
+ - **Recipe** — `uv run xtr-recipes recipes:sync` does the *Activate*, *Configure*, *Environment*
273
+ and *Ignore* steps below: it lists `JwtBundle`, writes a starting `<app>/config/jwt.py`,
274
+ `JWT_SECRET_KEY_PATH` (commented out) in `.env`, and ignores `/secrets/*.pem`. It prints the
275
+ keypair and firewall steps, which a recipe cannot make for you.
276
+ - **Activate** — `JwtBundle: {"all": True}` in `BUNDLES` in `<app>/bundles.py`, imported from
277
+ `xtr_security_jwt.bundle`. Then build the kernel with `concurrent_scoped_access=True` and call
278
+ `setup(app, kernel)` where the application is served, as the security family requires.
279
+ - **Brings along** — the [security bundle](../xtr-security), the
280
+ [clock](../xtr-clock) and the [event dispatcher](../xtr-event-dispatcher) always, because this
281
+ bundle requires them; the [console](../xtr-console) bundle when xtr-console is installed, for
282
+ the commands.
283
+ - **Configure** — the bundle needs a signing key: a `<app>/config/jwt.py` `@configure` function
284
+ returning a `JwtConfig(secret_key=…)`, and a firewall that lists `JwtAuthenticatorConfig()`
285
+ under its `authenticators` — see [Configure](#configure) and [Kernel / bundle](#kernel--bundle).
286
+ Without a `secret_key` the build fails, naming the missing setting.
287
+ - **Environment** — whatever the key material reads: an application usually points the key at a
288
+ file with `env("file:JWT_SECRET_KEY_PATH")`, so `JWT_SECRET_KEY_PATH` must be set.
289
+ - **Ignore** — the private key files the keypair command writes, when `--output-dir` points into
290
+ the project: add `secrets/*.pem` (or wherever they land) to `.gitignore`.
291
+ - **Remove** — drop the `BUNDLES` entry, delete `<app>/config/jwt.py` and the
292
+ `JwtAuthenticatorConfig` from the firewall, then `uv remove xtr-security-jwt`.
293
+ - **Check** — `debug:bundles` shows `jwt` as `listed` and `active`, and `security` as `required`;
294
+ `debug:firewall api` lists the firewall's `jwt` authenticator.
295
+
296
+ ## Kernel / bundle
297
+
298
+ ```python
299
+ # app/bundles.py
300
+ from xtr_security_jwt.bundle import JwtBundle
301
+
302
+ BUNDLES = {JwtBundle: {"all": True}}
303
+ ```
304
+
305
+ `JwtBundle` registers the signing chain — the key loader, the JWS provider, the encoder and the
306
+ token manager — under their interfaces, and prepends onto the security configuration an
307
+ authenticator factory keyed `jwt` and a user-provider factory keyed `jwt`. A firewall's
308
+ `JwtAuthenticatorConfig` then builds a `JwtAuthenticator` over the token manager, the main event
309
+ dispatcher, the configured token extractors and the firewall's user provider. It
310
+ requires the [security](../xtr-security), [clock](../xtr-clock) and
311
+ [event dispatcher](../xtr-event-dispatcher) bundles, and the [console](../xtr-console) bundle
312
+ when it is installed. It touches no cryptography until a service is asked for — but, needing a
313
+ signing key the application must choose, it fails the build when none is configured.
314
+
315
+ ## Errors
316
+
317
+ Everything the package raises derives from `SecurityError` (from
318
+ [xtr-security-core](../xtr-security-core)), so one `except SecurityError` catches it all:
319
+
320
+ | Error | Base | Raised when |
321
+ |---|---|---|
322
+ | `JwtFailureError` | `SecurityError` | The base of the signing and verification failures; carries a `reason` and any decoded `payload` |
323
+ | `JwtEncodeFailureError` | `JwtFailureError` | A token could not be signed — `invalid_config` or `unsigned_token` |
324
+ | `JwtDecodeFailureError` | `JwtFailureError` | A token could not be read — `invalid_token`, `expired_token` or `unverified_token` |
325
+ | `MissingClaimError` | `JwtFailureError` | A token lacks a claim a caller required |
326
+ | `ExpiredTokenError` | `AuthenticationError` | A caller presented an expired token — answered with a `401` "Expired JWT Token" |
327
+ | `InvalidTokenError` | `AuthenticationError` | A caller presented a malformed, unsigned or tampered token — `401` "Invalid JWT Token" |
328
+ | `MissingTokenError` | `AuthenticationError` | A request reached a protected resource with no token — `401` "JWT Token not found" |
329
+ | `InvalidPayloadError` | `AuthenticationError` | A verified token carried no user-id claim |
330
+
331
+ ## Layout
332
+
333
+ ```
334
+ xtr_security_jwt/
335
+ ├── events.py the event name constants
336
+ ├── encoder/ the encoder interfaces and the default encoder
337
+ ├── services/
338
+ │ ├── jwt_manager.py assembles, announces and signs a token
339
+ │ ├── jws_provider/ signs and verifies with joserfc
340
+ │ ├── key_loader/ reads the signing and verifying key material
341
+ │ └── payload_enrichment/ claims added to every token
342
+ ├── signature/ the created- and loaded-token value objects
343
+ ├── security/
344
+ │ ├── authenticator/ the JwtAuthenticator and its token
345
+ │ └── user/ the stateless JwtUser and its provider
346
+ ├── token_extractor/ where a firewall reads a token from
347
+ ├── event/ the events dispatched around a token's life
348
+ ├── exception/ the errors, one family under one base
349
+ ├── response/ the failure response
350
+ ├── command/ jwt:generate-keypair, jwt:generate-token, jwt:check-config
351
+ ├── factory/ the jwt authenticator factory
352
+ ├── user_provider/ the jwt user-provider factory
353
+ └── bundle/ JwtBundle and its configuration
354
+ ```
355
+
356
+ ## Development
357
+
358
+ Developed in the [python-xtr](https://github.com/xterr/python-xtr) monorepo, under
359
+ `packages/xtr-security-jwt`; run the commands below from there. The
360
+ `python-xtr-security-jwt` repository is a read-only copy, so send issues and pull requests to
361
+ the monorepo.
362
+
363
+ ```sh
364
+ uv sync --all-packages --all-extras
365
+ uv run ruff check && uv run ruff format --check && uv run basedpyright && uv run ty check && uv run pytest --cov
366
+ ```
367
+
368
+ ## License
369
+
370
+ MIT — see [LICENSE](LICENSE).