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.
- xtr_security_jwt/.agents/skills/xtr-security-jwt/SKILL.md +253 -0
- xtr_security_jwt/.agents/skills/xtr-security-jwt/references/configuration.md +75 -0
- xtr_security_jwt/.agents/skills/xtr-security-jwt/references/events-and-errors.md +48 -0
- xtr_security_jwt/__init__.py +8 -0
- xtr_security_jwt/bundle/__init__.py +29 -0
- xtr_security_jwt/bundle/encoder_config.py +31 -0
- xtr_security_jwt/bundle/jwt_authenticator_config.py +28 -0
- xtr_security_jwt/bundle/jwt_bundle.py +257 -0
- xtr_security_jwt/bundle/jwt_config.py +67 -0
- xtr_security_jwt/bundle/jwt_user_provider_config.py +29 -0
- xtr_security_jwt/bundle/token_extractors_configs.py +97 -0
- xtr_security_jwt/command/__init__.py +9 -0
- xtr_security_jwt/command/_registry.py +15 -0
- xtr_security_jwt/command/check_config_command.py +45 -0
- xtr_security_jwt/command/generate_key_pair_command.py +90 -0
- xtr_security_jwt/command/generate_token_command.py +99 -0
- xtr_security_jwt/encoder/__init__.py +13 -0
- xtr_security_jwt/encoder/default_jwt_encoder.py +101 -0
- xtr_security_jwt/encoder/header_aware_jwt_encoder_interface.py +38 -0
- xtr_security_jwt/encoder/jwt_encoder_interface.py +37 -0
- xtr_security_jwt/event/__init__.py +27 -0
- xtr_security_jwt/event/authentication_failure_event.py +62 -0
- xtr_security_jwt/event/authentication_success_event.py +42 -0
- xtr_security_jwt/event/jwt_authenticated_event.py +41 -0
- xtr_security_jwt/event/jwt_created_event.py +56 -0
- xtr_security_jwt/event/jwt_decoded_event.py +44 -0
- xtr_security_jwt/event/jwt_encoded_event.py +28 -0
- xtr_security_jwt/event/jwt_expired_event.py +19 -0
- xtr_security_jwt/event/jwt_failure_event_interface.py +33 -0
- xtr_security_jwt/event/jwt_invalid_event.py +19 -0
- xtr_security_jwt/event/jwt_not_found_event.py +18 -0
- xtr_security_jwt/events.py +62 -0
- xtr_security_jwt/exception/__init__.py +30 -0
- xtr_security_jwt/exception/expired_token_error.py +20 -0
- xtr_security_jwt/exception/invalid_payload_error.py +30 -0
- xtr_security_jwt/exception/invalid_token_error.py +20 -0
- xtr_security_jwt/exception/jwt_decode_failure_error.py +28 -0
- xtr_security_jwt/exception/jwt_encode_failure_error.py +24 -0
- xtr_security_jwt/exception/jwt_failure_error.py +47 -0
- xtr_security_jwt/exception/missing_claim_error.py +39 -0
- xtr_security_jwt/exception/missing_token_error.py +20 -0
- xtr_security_jwt/factory/__init__.py +7 -0
- xtr_security_jwt/factory/jwt_authenticator_factory.py +182 -0
- xtr_security_jwt/py.typed +0 -0
- xtr_security_jwt/recipe/__init__.py +16 -0
- xtr_security_jwt/recipe/files/config/jwt.py.tmpl +28 -0
- xtr_security_jwt/recipe/manifest.toml +36 -0
- xtr_security_jwt/response/__init__.py +7 -0
- xtr_security_jwt/response/jwt_authentication_failure_response.py +33 -0
- xtr_security_jwt/security/__init__.py +5 -0
- xtr_security_jwt/security/authenticator/__init__.py +7 -0
- xtr_security_jwt/security/authenticator/jwt_authenticator.py +257 -0
- xtr_security_jwt/security/authenticator/token/__init__.py +7 -0
- xtr_security_jwt/security/authenticator/token/jwt_post_authentication_token.py +42 -0
- xtr_security_jwt/security/user/__init__.py +9 -0
- xtr_security_jwt/security/user/jwt_user.py +58 -0
- xtr_security_jwt/security/user/jwt_user_interface.py +28 -0
- xtr_security_jwt/security/user/jwt_user_provider.py +66 -0
- xtr_security_jwt/services/__init__.py +13 -0
- xtr_security_jwt/services/jws_provider/__init__.py +8 -0
- xtr_security_jwt/services/jws_provider/joserfc_jws_provider.py +208 -0
- xtr_security_jwt/services/jws_provider/jws_provider_interface.py +36 -0
- xtr_security_jwt/services/jwt_manager.py +153 -0
- xtr_security_jwt/services/jwt_token_manager_interface.py +53 -0
- xtr_security_jwt/services/key_loader/__init__.py +15 -0
- xtr_security_jwt/services/key_loader/_algorithms.py +75 -0
- xtr_security_jwt/services/key_loader/abstract_key_loader.py +111 -0
- xtr_security_jwt/services/key_loader/key_dumper_interface.py +20 -0
- xtr_security_jwt/services/key_loader/key_loader_interface.py +47 -0
- xtr_security_jwt/services/key_loader/raw_key_loader.py +84 -0
- xtr_security_jwt/services/payload_enrichment/__init__.py +9 -0
- xtr_security_jwt/services/payload_enrichment/chain_enrichment.py +38 -0
- xtr_security_jwt/services/payload_enrichment/null_enrichment.py +26 -0
- xtr_security_jwt/services/payload_enrichment/random_jti_enrichment.py +32 -0
- xtr_security_jwt/services/payload_enrichment_interface.py +24 -0
- xtr_security_jwt/signature/__init__.py +8 -0
- xtr_security_jwt/signature/created_jws.py +36 -0
- xtr_security_jwt/signature/loaded_jws.py +123 -0
- xtr_security_jwt/token_extractor/__init__.py +19 -0
- xtr_security_jwt/token_extractor/authorization_header_token_extractor.py +45 -0
- xtr_security_jwt/token_extractor/chain_token_extractor.py +52 -0
- xtr_security_jwt/token_extractor/cookie_token_extractor.py +34 -0
- xtr_security_jwt/token_extractor/query_parameter_token_extractor.py +34 -0
- xtr_security_jwt/token_extractor/split_cookie_extractor.py +45 -0
- xtr_security_jwt/token_extractor/token_extractor_interface.py +24 -0
- xtr_security_jwt/user_provider/__init__.py +7 -0
- xtr_security_jwt/user_provider/jwt_user_factory.py +52 -0
- xtr_security_jwt-3.0.0.dist-info/METADATA +370 -0
- xtr_security_jwt-3.0.0.dist-info/RECORD +92 -0
- xtr_security_jwt-3.0.0.dist-info/WHEEL +4 -0
- xtr_security_jwt-3.0.0.dist-info/entry_points.txt +6 -0
- 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
|