xtr-security-jwt 3.0.0__py3-none-any.whl

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/.agents/skills/xtr-security-jwt/SKILL.md +253 -0
  2. xtr_security_jwt/.agents/skills/xtr-security-jwt/references/configuration.md +75 -0
  3. xtr_security_jwt/.agents/skills/xtr-security-jwt/references/events-and-errors.md +48 -0
  4. xtr_security_jwt/__init__.py +8 -0
  5. xtr_security_jwt/bundle/__init__.py +29 -0
  6. xtr_security_jwt/bundle/encoder_config.py +31 -0
  7. xtr_security_jwt/bundle/jwt_authenticator_config.py +28 -0
  8. xtr_security_jwt/bundle/jwt_bundle.py +257 -0
  9. xtr_security_jwt/bundle/jwt_config.py +67 -0
  10. xtr_security_jwt/bundle/jwt_user_provider_config.py +29 -0
  11. xtr_security_jwt/bundle/token_extractors_configs.py +97 -0
  12. xtr_security_jwt/command/__init__.py +9 -0
  13. xtr_security_jwt/command/_registry.py +15 -0
  14. xtr_security_jwt/command/check_config_command.py +45 -0
  15. xtr_security_jwt/command/generate_key_pair_command.py +90 -0
  16. xtr_security_jwt/command/generate_token_command.py +99 -0
  17. xtr_security_jwt/encoder/__init__.py +13 -0
  18. xtr_security_jwt/encoder/default_jwt_encoder.py +101 -0
  19. xtr_security_jwt/encoder/header_aware_jwt_encoder_interface.py +38 -0
  20. xtr_security_jwt/encoder/jwt_encoder_interface.py +37 -0
  21. xtr_security_jwt/event/__init__.py +27 -0
  22. xtr_security_jwt/event/authentication_failure_event.py +62 -0
  23. xtr_security_jwt/event/authentication_success_event.py +42 -0
  24. xtr_security_jwt/event/jwt_authenticated_event.py +41 -0
  25. xtr_security_jwt/event/jwt_created_event.py +56 -0
  26. xtr_security_jwt/event/jwt_decoded_event.py +44 -0
  27. xtr_security_jwt/event/jwt_encoded_event.py +28 -0
  28. xtr_security_jwt/event/jwt_expired_event.py +19 -0
  29. xtr_security_jwt/event/jwt_failure_event_interface.py +33 -0
  30. xtr_security_jwt/event/jwt_invalid_event.py +19 -0
  31. xtr_security_jwt/event/jwt_not_found_event.py +18 -0
  32. xtr_security_jwt/events.py +62 -0
  33. xtr_security_jwt/exception/__init__.py +30 -0
  34. xtr_security_jwt/exception/expired_token_error.py +20 -0
  35. xtr_security_jwt/exception/invalid_payload_error.py +30 -0
  36. xtr_security_jwt/exception/invalid_token_error.py +20 -0
  37. xtr_security_jwt/exception/jwt_decode_failure_error.py +28 -0
  38. xtr_security_jwt/exception/jwt_encode_failure_error.py +24 -0
  39. xtr_security_jwt/exception/jwt_failure_error.py +47 -0
  40. xtr_security_jwt/exception/missing_claim_error.py +39 -0
  41. xtr_security_jwt/exception/missing_token_error.py +20 -0
  42. xtr_security_jwt/factory/__init__.py +7 -0
  43. xtr_security_jwt/factory/jwt_authenticator_factory.py +182 -0
  44. xtr_security_jwt/py.typed +0 -0
  45. xtr_security_jwt/recipe/__init__.py +16 -0
  46. xtr_security_jwt/recipe/files/config/jwt.py.tmpl +28 -0
  47. xtr_security_jwt/recipe/manifest.toml +36 -0
  48. xtr_security_jwt/response/__init__.py +7 -0
  49. xtr_security_jwt/response/jwt_authentication_failure_response.py +33 -0
  50. xtr_security_jwt/security/__init__.py +5 -0
  51. xtr_security_jwt/security/authenticator/__init__.py +7 -0
  52. xtr_security_jwt/security/authenticator/jwt_authenticator.py +257 -0
  53. xtr_security_jwt/security/authenticator/token/__init__.py +7 -0
  54. xtr_security_jwt/security/authenticator/token/jwt_post_authentication_token.py +42 -0
  55. xtr_security_jwt/security/user/__init__.py +9 -0
  56. xtr_security_jwt/security/user/jwt_user.py +58 -0
  57. xtr_security_jwt/security/user/jwt_user_interface.py +28 -0
  58. xtr_security_jwt/security/user/jwt_user_provider.py +66 -0
  59. xtr_security_jwt/services/__init__.py +13 -0
  60. xtr_security_jwt/services/jws_provider/__init__.py +8 -0
  61. xtr_security_jwt/services/jws_provider/joserfc_jws_provider.py +208 -0
  62. xtr_security_jwt/services/jws_provider/jws_provider_interface.py +36 -0
  63. xtr_security_jwt/services/jwt_manager.py +153 -0
  64. xtr_security_jwt/services/jwt_token_manager_interface.py +53 -0
  65. xtr_security_jwt/services/key_loader/__init__.py +15 -0
  66. xtr_security_jwt/services/key_loader/_algorithms.py +75 -0
  67. xtr_security_jwt/services/key_loader/abstract_key_loader.py +111 -0
  68. xtr_security_jwt/services/key_loader/key_dumper_interface.py +20 -0
  69. xtr_security_jwt/services/key_loader/key_loader_interface.py +47 -0
  70. xtr_security_jwt/services/key_loader/raw_key_loader.py +84 -0
  71. xtr_security_jwt/services/payload_enrichment/__init__.py +9 -0
  72. xtr_security_jwt/services/payload_enrichment/chain_enrichment.py +38 -0
  73. xtr_security_jwt/services/payload_enrichment/null_enrichment.py +26 -0
  74. xtr_security_jwt/services/payload_enrichment/random_jti_enrichment.py +32 -0
  75. xtr_security_jwt/services/payload_enrichment_interface.py +24 -0
  76. xtr_security_jwt/signature/__init__.py +8 -0
  77. xtr_security_jwt/signature/created_jws.py +36 -0
  78. xtr_security_jwt/signature/loaded_jws.py +123 -0
  79. xtr_security_jwt/token_extractor/__init__.py +19 -0
  80. xtr_security_jwt/token_extractor/authorization_header_token_extractor.py +45 -0
  81. xtr_security_jwt/token_extractor/chain_token_extractor.py +52 -0
  82. xtr_security_jwt/token_extractor/cookie_token_extractor.py +34 -0
  83. xtr_security_jwt/token_extractor/query_parameter_token_extractor.py +34 -0
  84. xtr_security_jwt/token_extractor/split_cookie_extractor.py +45 -0
  85. xtr_security_jwt/token_extractor/token_extractor_interface.py +24 -0
  86. xtr_security_jwt/user_provider/__init__.py +7 -0
  87. xtr_security_jwt/user_provider/jwt_user_factory.py +52 -0
  88. xtr_security_jwt-3.0.0.dist-info/METADATA +370 -0
  89. xtr_security_jwt-3.0.0.dist-info/RECORD +92 -0
  90. xtr_security_jwt-3.0.0.dist-info/WHEEL +4 -0
  91. xtr_security_jwt-3.0.0.dist-info/entry_points.txt +6 -0
  92. xtr_security_jwt-3.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,253 @@
1
+ ---
2
+ name: xtr-security-jwt
3
+ description: How to issue and accept self-issued JSON Web Tokens with xtr-security-jwt. Use when an application must mint an access token for one of its users, write a /token or login endpoint, accept a bearer token on a protected route, configure a signing key or key pair, pass phrase or extra trusted public keys, pick a signature algorithm (RS256, ES256, EdDSA, HS256), publish a JWK set, read a token from a header, cookie or query parameter, add a claim such as jti or a tenant to every token, listen to token created, encoded, decoded, expired, invalid or not-found events, run jwt:generate-keypair, jwt:generate-token or jwt:check-config, or activate JwtBundle and fix the build failure it raises until a signing key and the firewall entry are configured.
4
+ ---
5
+
6
+ # xtr-security-jwt
7
+
8
+ The security family verifies bearer tokens issued elsewhere; this package issues them. An
9
+ application signs a token for one of its users, hands it to a client, and accepts it back on a
10
+ protected route, with no authorization server in the loop.
11
+
12
+ Firewalls, access control, voters and user providers belong to the family: load the
13
+ `xtr-security` skill for those. This one covers only the JWT half.
14
+
15
+ ## Quick reference
16
+
17
+ - Inject `JwtTokenManagerInterface` to mint a token: `await tokens.create(user)`.
18
+ - `JwtBundle` **fails the build** until `JwtConfig(secret_key=...)` is configured. There is no
19
+ zero-config path. See [Use in an application](#use-in-an-application).
20
+ - A firewall accepts tokens by listing `JwtAuthenticatorConfig()` under its `authenticators`;
21
+ a deployment with no user store adds `JwtUserProviderConfig()` as a provider.
22
+ - Tokens arrive in the `Authorization: Bearer` header by default; cookies, a query parameter and
23
+ split cookies opt in through `TokenExtractorsConfig`.
24
+ - `jwt:generate-keypair` mints a key; `jwt:check-config` proves the configured one works.
25
+ - Every failure derives from `SecurityError`.
26
+
27
+ ## Issue a token
28
+
29
+ ```python
30
+ from fastapi import FastAPI
31
+ from xtr_dependency_injection import Injected
32
+ from xtr_security_http import Firewall
33
+ from xtr_security_jwt import JwtTokenManagerInterface
34
+
35
+ app = FastAPI(dependencies=[Firewall()])
36
+
37
+
38
+ @app.post("/token")
39
+ async def issue_token(
40
+ identifier: str,
41
+ tokens: Injected[JwtTokenManagerInterface],
42
+ ) -> dict[str, str]:
43
+ user = await _authenticated_user(identifier) # your code checks the password
44
+ return {"access_token": await tokens.create(user)}
45
+ ```
46
+
47
+ The manager never reads any part of the token from client input: it assembles the claims from
48
+ the user you hand it. Authenticate that user **before** calling `create`, and throttle the
49
+ endpoint. Reading the user back on a protected route is `CurrentUser()` from the family.
50
+
51
+ | Call | Does |
52
+ | --- | --- |
53
+ | `await tokens.create(user)` | Mints a signed token carrying the user's identifier and roles |
54
+ | `await tokens.create_from_payload(user, payload)` | The same, starting from claims you supply |
55
+ | `await tokens.parse(token)` | Verifies a compact token and returns its claims |
56
+ | `await tokens.decode(security_token)` | Claims out of a security token, or `False` when it has none |
57
+ | `tokens.get_user_id_claim()` | The claim the identifier is written into |
58
+
59
+ `create` writes `roles`, the identifier under `user_id_claim`, and the time claims the algorithm
60
+ and `token_ttl` imply (`iat`, `exp`).
61
+
62
+ ## Accept a token on a firewall
63
+
64
+ ```python
65
+ # app/config/security.py
66
+ from xtr_dependency_injection import configure
67
+ from xtr_security.bundle import AccessControlConfig, FirewallConfig, SecurityConfig
68
+ from xtr_security_jwt.bundle import JwtAuthenticatorConfig, JwtUserProviderConfig
69
+
70
+
71
+ @configure
72
+ def security() -> SecurityConfig:
73
+ return SecurityConfig(
74
+ providers={"jwt_users": JwtUserProviderConfig()},
75
+ firewalls={
76
+ "api": FirewallConfig(
77
+ pattern=r"^/api",
78
+ provider="jwt_users",
79
+ authenticators=(JwtAuthenticatorConfig(),),
80
+ ),
81
+ },
82
+ access_control=(AccessControlConfig(path=r"^/api", attribute="IS_AUTHENTICATED"),),
83
+ )
84
+ ```
85
+
86
+ - `JwtAuthenticatorConfig(provider=None, authenticator=None)` takes nothing else: the keys, the
87
+ algorithm and the extractors all come from `JwtConfig`. `provider` overrides the firewall's
88
+ user provider; `authenticator` names a registered service to build instead of the default.
89
+ - `JwtUserProviderConfig(user_class=JwtUser)` is the stateless provider, and `JwtUser` carries an
90
+ identifier and the `roles` claim, nothing else. A deployment with a user store names its own
91
+ provider instead and drops this one.
92
+
93
+ ## Configure keys and claims
94
+
95
+ ```python
96
+ # app/config/jwt.py
97
+ from xtr_dependency_injection import configure, env
98
+ from xtr_security_jwt.bundle import EncoderConfig, JwtConfig
99
+
100
+
101
+ @configure
102
+ def jwt() -> JwtConfig:
103
+ return JwtConfig(
104
+ secret_key=env("file:JWT_SECRET_KEY_PATH"),
105
+ encoder=EncoderConfig(signature_algorithm="RS256"),
106
+ token_ttl=900,
107
+ user_id_claim="username",
108
+ )
109
+ ```
110
+
111
+ The fields that matter most:
112
+
113
+ | Field | Default | What it is |
114
+ | --- | --- | --- |
115
+ | `secret_key` | `None` | **Required.** The private key or shared secret, as key text or a file path |
116
+ | `public_key` | `None` | The verifying key, or `None` to derive it from the private key |
117
+ | `token_ttl` | `3600` | Seconds a minted token lives |
118
+ | `clock_skew` | `0` | Seconds of skew tolerated on a verified token's time claims |
119
+ | `user_id_claim` | `"username"` | The claim the identifier is written into and read back from |
120
+
121
+ Every field, the supported algorithms, the pass phrase, extra trusted public keys and the four
122
+ token extractors: [references/configuration.md](references/configuration.md).
123
+
124
+ ## Add a claim to every token
125
+
126
+ Register a `PayloadEnrichmentInterface`; the manager chains every one it is given. Never reach
127
+ for the container to stamp a claim, and never edit a signed token.
128
+
129
+ ```python
130
+ from xtr_security_core.user.user_interface import UserInterface
131
+ from xtr_security_jwt.services.payload_enrichment_interface import PayloadEnrichmentInterface
132
+
133
+
134
+ class TenantEnrichment(PayloadEnrichmentInterface):
135
+ def enrich(self, user: UserInterface, payload: dict[str, object]) -> None:
136
+ payload["tenant"] = "acme" # edited in place, before signing
137
+ ```
138
+
139
+ `RandomJtiEnrichment`, in `xtr_security_jwt.services.payload_enrichment.random_jti_enrichment`,
140
+ ships for a unique token id.
141
+
142
+ ## Listen to a token's life
143
+
144
+ Listen under a constant on `Events`, never by importing the event class. `JWT_CREATED` shapes
145
+ the claims before signing, `JWT_DECODED` may reject a verified token, and `JWT_EXPIRED`,
146
+ `JWT_INVALID` and `JWT_NOT_FOUND` are the refusals. All nine, with what a listener may do to
147
+ each: [references/events-and-errors.md](references/events-and-errors.md).
148
+
149
+ ## Commands
150
+
151
+ With the `console` extra and a console bundle active:
152
+
153
+ | Command | Does |
154
+ | --- | --- |
155
+ | `jwt:generate-keypair [--algorithm RS256] [--kid KID] [--output-dir DIR]` | Mints a key, prints the private PEM and public JWK set, or writes both into `DIR` named by key id |
156
+ | `jwt:generate-token IDENTIFIER [--provider NAME]` | Loads the user through a configured provider and prints a token for it |
157
+ | `jwt:check-config` | Signs a probe token and reads it back, proving the configured keys work |
158
+
159
+ ## Testing
160
+
161
+ Boot the kernel and resolve the manager; nothing is mocked, and a throwaway key file keeps the
162
+ test self-contained.
163
+
164
+ ```python
165
+ import pytest
166
+ from xtr_dependency_injection import Kernel
167
+ from xtr_security_core.user.in_memory_user import InMemoryUser
168
+ from xtr_security_jwt import JwtTokenManagerInterface
169
+ from xtr_security_jwt.bundle import JwtBundle
170
+
171
+
172
+ @pytest.mark.anyio
173
+ async def test_a_token_names_its_user(tmp_path) -> None:
174
+ key = tmp_path / "private.pem"
175
+ key.write_text(PRIVATE_PEM)
176
+ kernel = Kernel(
177
+ "app",
178
+ env="test",
179
+ bundles={JwtBundle: {"all": True}},
180
+ concurrent_scoped_access=True,
181
+ environ={"JWT_SECRET_KEY_PATH": str(key)},
182
+ )
183
+
184
+ async with await kernel.build().boot() as booted:
185
+ tokens = await booted.container.get(JwtTokenManagerInterface)
186
+ claims = await tokens.parse(await tokens.create(InMemoryUser("ada", roles=["ROLE_USER"])))
187
+
188
+ assert claims["username"] == "ada"
189
+ ```
190
+
191
+ - Generate the key in a fixture rather than committing one:
192
+ `RSAKey.generate_key(2048).as_pem(private=True).decode()`, from `joserfc.jwk`.
193
+ - Drive a served application through `httpx.ASGITransport` inside its own lifespan, so
194
+ `setup(app, kernel)` builds and boots the kernel for the test.
195
+ - Replace a service with `boot_for_test(kernel, overrides={...})` from
196
+ `xtr_dependency_injection.testing`.
197
+
198
+ ## Use in an application
199
+
200
+ `uv run xtr-recipes recipes:sync` applies the recipe shipped with this package: it lists `JwtBundle`,
201
+ writes a starting `config/jwt.py`, `JWT_SECRET_KEY_PATH` (commented out) in `.env`, and ignores
202
+ `/secrets/*.pem`. That is the steps below a recipe can do; the keypair and firewall steps it prints
203
+ for you to make.
204
+
205
+ 1. **Install** — `uv add xtr-security-jwt`; add `[console]` for the commands.
206
+ 2. **Mint a key** — `jwt:generate-keypair --algorithm RS256 --output-dir secrets/`. The private
207
+ PEM signs, the public JWK set verifies.
208
+ 3. **Activate** — `JwtBundle: {"all": True}` in `BUNDLES` in `<app>/bundles.py`, from
209
+ `xtr_security_jwt.bundle`. Build the kernel with `concurrent_scoped_access=True` and call
210
+ `setup(app, kernel)` where the application is served, as the security family requires.
211
+ 4. **Configure, and you must** — this bundle has no zero-config path. Write
212
+ `<app>/config/jwt.py` returning a `JwtConfig(secret_key=...)` and add
213
+ `JwtAuthenticatorConfig()` to a firewall's `authenticators` in `<app>/config/security.py`.
214
+ Without a `secret_key` the build fails with `InvalidConfigurationError`, naming the missing
215
+ setting and the config function to write.
216
+ 5. **Brings along** — the security, clock and event dispatcher bundles always; the console
217
+ bundle when xtr-console is installed.
218
+ 6. **Environment** — with `secret_key=env("file:JWT_SECRET_KEY_PATH")`, set
219
+ `JWT_SECRET_KEY_PATH`. It resolves at boot, not at build.
220
+ 7. **Ignore** — `.gitignore` the private keys when they land in the project: `secrets/*.pem`.
221
+ 8. **Check** — `debug:bundles` shows `jwt` as `listed` and `active` and `security` as
222
+ `required`; `debug:firewall api` lists the `jwt` authenticator; `jwt:check-config` proves the
223
+ keys sign and verify.
224
+ 9. **Remove** — drop the `BUNDLES` entry, delete `<app>/config/jwt.py` and the
225
+ `JwtAuthenticatorConfig` from the firewall, then `uv remove xtr-security-jwt`.
226
+
227
+ ## Errors
228
+
229
+ Import from `xtr_security_jwt.exception`. All derive from `SecurityError`, so one
230
+ `except SecurityError` catches every one.
231
+
232
+ | Error | Raised when |
233
+ | --- | --- |
234
+ | `JwtFailureError` | The base of the signing and verification failures; carries a `reason` and any decoded `payload` |
235
+ | `JwtEncodeFailureError` | `tokens.create` could not sign |
236
+ | `JwtDecodeFailureError` | `tokens.parse` could not read, verify or accept the expiry of a token |
237
+ | `MissingClaimError` | A token lacks a claim a caller required |
238
+ | `ExpiredTokenError`, `InvalidTokenError`, `MissingTokenError`, `InvalidPayloadError` | The firewall's own `401` answers; it raises and handles these itself |
239
+
240
+ Reasons, messages, and the two errors from outside the family:
241
+ [references/events-and-errors.md](references/events-and-errors.md).
242
+
243
+ ## Do not
244
+
245
+ - Do not build a token from anything a client sent. Authenticate the user, then call `create`.
246
+ - Do not commit a private key or write one into `JwtConfig` as literal text; point `secret_key`
247
+ at a file with `env("file:...")`.
248
+ - Do not expect `JwtBundle` to boot unconfigured. It is the one bundle here that refuses to.
249
+ - Do not verify a token by hand with a JOSE library; `tokens.parse` applies the configured keys,
250
+ algorithm, skew and expiry rules.
251
+ - Do not enable the query-parameter extractor unless you accept tokens in logs and referrers.
252
+ - Do not reach for the family's generic bearer authenticator for a self-issued token; that one
253
+ is for tokens issued elsewhere.
@@ -0,0 +1,75 @@
1
+ # JwtConfig, field by field
2
+
3
+ `from xtr_security_jwt.bundle import JwtConfig`. A frozen dataclass; every field has a default,
4
+ but the bundle refuses to build without `secret_key`.
5
+
6
+ | Field | Default | What it is |
7
+ | --- | --- | --- |
8
+ | `secret_key` | `None` | **Required.** The private key or shared secret, as key text or a file path |
9
+ | `public_key` | `None` | The verifying key, or `None` to derive it from the private key |
10
+ | `additional_public_keys` | `()` | Files of extra public keys a token may also verify against |
11
+ | `pass_phrase` | `""` | The pass phrase the private key is encrypted with |
12
+ | `token_ttl` | `3600` | Seconds a minted token lives; must be positive |
13
+ | `allow_no_expiration` | `False` | Whether a token with no `exp` is honoured |
14
+ | `clock_skew` | `0` | Seconds of skew tolerated on a verified token's time claims; must not be negative |
15
+ | `encoder` | `EncoderConfig()` | How a token is signed |
16
+ | `user_id_claim` | `"username"` | The claim the identifier is written into and read back from |
17
+ | `token_extractors` | `TokenExtractorsConfig()` | Where a firewall reads a token from |
18
+
19
+ A bad combination raises `InvalidArgumentError` (from `xtr_security_core.exception`) as the
20
+ config is built: a negative `clock_skew`, a `token_ttl` of zero or less, an empty
21
+ `user_id_claim`.
22
+
23
+ ## Key material
24
+
25
+ `secret_key` and `public_key` each take either the key text itself or the path of a file holding
26
+ it. An existing file wins, so a key that happens to look like a path is still read from disk
27
+ when that path exists. `additional_public_keys` must each be a readable **file**: they are other
28
+ issuers' keys a deployment trusts.
29
+
30
+ Point the key at a file through the environment rather than embedding it:
31
+
32
+ ```python
33
+ JwtConfig(secret_key=env("file:JWT_SECRET_KEY_PATH"), pass_phrase=env("JWT_PASSPHRASE"))
34
+ ```
35
+
36
+ The placeholder is resolved at boot, not at build, so the file need not exist while compiling.
37
+
38
+ ## Algorithm
39
+
40
+ `EncoderConfig(service=None, signature_algorithm="RS256")`. `service` names a registered encoder
41
+ to sign with instead of the default joserfc one.
42
+
43
+ | Family | Algorithms | Key |
44
+ | --- | --- | --- |
45
+ | RSA | `RS256`, `RS384`, `RS512`, `PS256`, `PS384`, `PS512` | An RSA key pair |
46
+ | Elliptic curve | `ES256`, `ES384`, `ES512` | An EC key pair |
47
+ | Edwards curve | `EdDSA` | An OKP key pair |
48
+ | Shared secret | `HS256`, `HS384`, `HS512` | One secret; `secret_key` is it, and there is no pair |
49
+
50
+ Anything else raises `InvalidArgumentError`, naming the allowed set.
51
+
52
+ ## Token extractors
53
+
54
+ `TokenExtractorsConfig` groups the four places a firewall may read a token from. All four
55
+ configs import from `xtr_security_jwt.bundle`.
56
+
57
+ | Config | Default | Reads |
58
+ | --- | --- | --- |
59
+ | `AuthorizationHeaderExtractorConfig(enabled=True, prefix="Bearer", name="Authorization")` | on | A header value, after the scheme prefix. An empty `prefix` takes the whole value |
60
+ | `CookieExtractorConfig(enabled=False, name="BEARER")` | off | A named cookie |
61
+ | `QueryParameterExtractorConfig(enabled=False, name="bearer")` | off | A named query parameter |
62
+ | `SplitCookieExtractorConfig(enabled=False, cookies=())` | off | Several named cookies, rejoined in the order given |
63
+
64
+ ```python
65
+ from xtr_security_jwt.bundle import CookieExtractorConfig, JwtConfig, TokenExtractorsConfig
66
+
67
+ JwtConfig(
68
+ secret_key=...,
69
+ token_extractors=TokenExtractorsConfig(
70
+ cookie=CookieExtractorConfig(enabled=True, name="access_token"),
71
+ ),
72
+ )
73
+ ```
74
+
75
+ Enabling more than one means a token is accepted from any of them, first match winning.
@@ -0,0 +1,48 @@
1
+ # Events and errors
2
+
3
+ ## Events
4
+
5
+ Every event has a name constant on `Events` (`from xtr_security_jwt import Events`). A listener
6
+ names the constant; it never imports the event class only to name it.
7
+
8
+ | Name | Dispatched | A listener may |
9
+ | --- | --- | --- |
10
+ | `Events.JWT_CREATED` | Before a token is signed | Shape the claims and the header |
11
+ | `Events.JWT_ENCODED` | After a token is signed | Observe the finished token |
12
+ | `Events.JWT_DECODED` | After a token verifies | Reject it |
13
+ | `Events.JWT_AUTHENTICATED` | After a token authenticated a request | Observe the payload |
14
+ | `Events.JWT_EXPIRED` | An expired token is refused | Observe, or shape the response |
15
+ | `Events.JWT_INVALID` | A bad token is refused | Observe, or shape the response |
16
+ | `Events.JWT_NOT_FOUND` | A protected request carried no token | Observe, or shape the response |
17
+ | `Events.AUTHENTICATION_SUCCESS` | A token is issued to a client | Add to the response data |
18
+ | `Events.AUTHENTICATION_FAILURE` | A token authentication failed | Shape the response |
19
+
20
+ Prefer a `PayloadEnrichmentInterface` over a `JWT_CREATED` listener when all you do is add a
21
+ claim to every token: it is one object with one job, and it needs no dispatcher.
22
+
23
+ ## Errors
24
+
25
+ Import from `xtr_security_jwt.exception`. All derive from `SecurityError`
26
+ (`xtr_security_core.exception`), so one `except SecurityError` catches every one.
27
+
28
+ | Error | Base | Raised when |
29
+ | --- | --- | --- |
30
+ | `JwtFailureError` | `SecurityError` | The base of the signing and verification failures; carries a `reason` and any decoded `payload` |
31
+ | `JwtEncodeFailureError` | `JwtFailureError` | A token could not be signed: reason `invalid_config` or `unsigned_token` |
32
+ | `JwtDecodeFailureError` | `JwtFailureError` | A token could not be read: reason `invalid_token`, `expired_token` or `unverified_token` |
33
+ | `MissingClaimError` | `JwtFailureError` | A token lacks a claim a caller required |
34
+ | `ExpiredTokenError` | `AuthenticationError` | A caller presented an expired token; answered `401` "Expired JWT Token" |
35
+ | `InvalidTokenError` | `AuthenticationError` | A malformed, unsigned or tampered token; `401` "Invalid JWT Token" |
36
+ | `MissingTokenError` | `AuthenticationError` | A protected request carried no token; `401` "JWT Token not found" |
37
+ | `InvalidPayloadError` | `AuthenticationError` | A verified token carried no user-id claim |
38
+
39
+ The four `AuthenticationError` ones are the firewall's answers to a request: it turns them into
40
+ the `401` itself, so an application rarely catches them. The `JwtFailureError` family is what
41
+ `tokens.create` and `tokens.parse` raise at you.
42
+
43
+ Two more come from outside the family:
44
+
45
+ - `InvalidArgumentError` (`xtr_security_core.exception`) for a `JwtConfig` the library will not
46
+ accept, or an algorithm it will not sign with.
47
+ - `InvalidConfigurationError` (`xtr_security.bundle`) at build time, when no `secret_key` is
48
+ configured. It names the missing setting and the config function to write.
@@ -0,0 +1,8 @@
1
+ """Self-issued JSON Web Tokens for xtr security: encoder, token manager and authenticator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .events import Events
6
+ from .services.jwt_token_manager_interface import JwtTokenManagerInterface
7
+
8
+ __all__ = ["Events", "JwtTokenManagerInterface"]
@@ -0,0 +1,29 @@
1
+ """The xtr-dependency-injection bundle for xtr-security-jwt."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .encoder_config import EncoderConfig
6
+ from .jwt_authenticator_config import JwtAuthenticatorConfig
7
+ from .jwt_bundle import JwtBundle
8
+ from .jwt_config import JwtConfig
9
+ from .jwt_user_provider_config import JwtUserProviderConfig
10
+ from .token_extractors_configs import (
11
+ AuthorizationHeaderExtractorConfig,
12
+ CookieExtractorConfig,
13
+ QueryParameterExtractorConfig,
14
+ SplitCookieExtractorConfig,
15
+ TokenExtractorsConfig,
16
+ )
17
+
18
+ __all__ = [
19
+ "AuthorizationHeaderExtractorConfig",
20
+ "CookieExtractorConfig",
21
+ "EncoderConfig",
22
+ "JwtAuthenticatorConfig",
23
+ "JwtBundle",
24
+ "JwtConfig",
25
+ "JwtUserProviderConfig",
26
+ "QueryParameterExtractorConfig",
27
+ "SplitCookieExtractorConfig",
28
+ "TokenExtractorsConfig",
29
+ ]
@@ -0,0 +1,31 @@
1
+ """How a token is signed, as configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from xtr_security_jwt.services.key_loader._algorithms import key_type_for_algorithm
8
+
9
+ __all__ = ["EncoderConfig"]
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class EncoderConfig:
14
+ """The encoder a token is signed with, and the algorithm it signs with.
15
+
16
+ Attributes:
17
+ service: A registered encoder to sign with instead of the default, or
18
+ ``None`` to sign with the joserfc encoder over the configured keys.
19
+ signature_algorithm: The signature algorithm the default encoder uses.
20
+ """
21
+
22
+ service: type | None = None
23
+ signature_algorithm: str = "RS256"
24
+
25
+ def __post_init__(self) -> None:
26
+ """Refuse an algorithm this library will not sign or verify with.
27
+
28
+ Raises:
29
+ InvalidArgumentError: When the algorithm is not supported.
30
+ """
31
+ _ = key_type_for_algorithm(self.signature_algorithm)
@@ -0,0 +1,28 @@
1
+ """The firewall configuration for the self-issued-token authenticator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ __all__ = ["JwtAuthenticatorConfig"]
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class JwtAuthenticatorConfig:
12
+ """A firewall's ``jwt`` authenticator, mirroring the reference ``jwt: ~`` node.
13
+
14
+ A firewall accepts self-issued tokens by listing one of these under its
15
+ authenticators; there is nothing more to set — the keys, the algorithm and
16
+ the extractors all come from the bundle's own configuration. The optional
17
+ ``provider`` overrides the firewall's user provider, and ``authenticator``
18
+ names a registered authenticator service to build instead of the default.
19
+
20
+ Attributes:
21
+ provider: The user provider to load the token's user through, or ``None``
22
+ to use the firewall's own.
23
+ authenticator: A registered authenticator service to use instead of the
24
+ one this bundle builds, or ``None`` for the default.
25
+ """
26
+
27
+ provider: str | None = None
28
+ authenticator: type | None = None